Cut core/view and shell/view comment bloat ~65% (comments only, zero code change)
This commit is contained in:
+130
-269
@@ -1,12 +1,7 @@
|
||||
// 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.
|
||||
// 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); this file owns REAPER reads/writes
|
||||
// and the snapshot-before-park ordering.
|
||||
|
||||
#include "shell/view/view.h"
|
||||
|
||||
@@ -37,8 +32,8 @@
|
||||
#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.
|
||||
// Lane minting (D2 Wave 3): item-side lane reads/writes to assign each item to
|
||||
// its mode's managed lane.
|
||||
#define REAPERAPI_WANT_CountTrackMediaItems
|
||||
#define REAPERAPI_WANT_GetTrackMediaItem
|
||||
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
||||
@@ -47,7 +42,6 @@
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
|
||||
using view::buildFolderTree;
|
||||
using view::isOnManualLane;
|
||||
using view::managedLaneKey;
|
||||
@@ -56,35 +50,24 @@ using view::TrackFolderEntry;
|
||||
|
||||
namespace {
|
||||
|
||||
// Track fixed-lane mode value (I_FREEMODE=2). See SDK: 0=normal, 1=free item
|
||||
// positioning, 2=fixed lanes.
|
||||
// I_FREEMODE value for fixed lanes. 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.
|
||||
// C_LANESCOLLAPSED=2: render a tool-split track like a normal single-lane
|
||||
// track showing only the playing lane (SDK: 1=collapsed, 2=hidden-lanes-exist
|
||||
// but displays as non-fixed-lane).
|
||||
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.
|
||||
// C_LANESETTINGS &32 = hide per-lane buttons; OR'd in, never clobbering the
|
||||
// mask. Deliberately NOT setting &1 (auto-remove empty lanes): the lazy-mint
|
||||
// decision never mints an empty lane, so &1 buys nothing and risks REAPER
|
||||
// silently removing a managed lane out from under the ownership index.
|
||||
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.
|
||||
// Makes a tool-split track's display read as an ordinary track. Gated by every
|
||||
// caller on the tool-driven transition INTO fixed lanes (freeMode != 2 before
|
||||
// the flip) — a track already in fixed-lane mode (the user's own) never
|
||||
// reaches this, so a user's comp-lane display prefs are never stomped.
|
||||
void applyTransparentLaneDisplay(MediaTrack* tr) {
|
||||
SetMediaTrackInfo_Value(tr, "C_LANESCOLLAPSED",
|
||||
static_cast<double>(kLanesDisplayAsNormal));
|
||||
@@ -93,8 +76,6 @@ void applyTransparentLaneDisplay(MediaTrack* tr) {
|
||||
static_cast<double>(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";
|
||||
@@ -105,11 +86,9 @@ const char* flagParm(Flag f) {
|
||||
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.
|
||||
// The master track is not enumerated by GetTrack (index space excludes it),
|
||||
// so it can never enter the tree — the master-untouched invariant holds by
|
||||
// construction. Also caches each MediaTrack* by GUID for later resolve().
|
||||
std::vector<TrackFolderEntry> readFolderEntries(
|
||||
ReaProject* proj,
|
||||
std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
||||
@@ -137,10 +116,8 @@ MediaTrack* resolve(const std::vector<std::pair<std::string, MediaTrack*>>& hand
|
||||
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).
|
||||
// Captures prior driven-flag state before parking. Never reads B_MUTE/I_SOLO;
|
||||
// ints preserve whatever REAPER reported (TrackSnapshot's defensive contract).
|
||||
TrackSnapshot snapshotTrack(MediaTrack* tr) {
|
||||
TrackSnapshot snap;
|
||||
snap.showInTcp = static_cast<int>(GetMediaTrackInfo_Value(tr, "B_SHOWINTCP"));
|
||||
@@ -156,16 +133,14 @@ TrackSnapshot snapshotTrack(MediaTrack* tr) {
|
||||
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<TrackFlagOp>& flags) {
|
||||
for (const TrackFlagOp& op : flags) {
|
||||
SetMediaTrackInfo_Value(tr, flagParm(op.flag), static_cast<double>(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.
|
||||
// The pure park plan leaves fxOffline empty by design; expand it here from the
|
||||
// live FX count.
|
||||
void parkFxOffline(MediaTrack* tr) {
|
||||
int fxCount = TrackFX_GetCount(tr);
|
||||
for (int fx = 0; fx < fxCount; ++fx) {
|
||||
@@ -173,15 +148,13 @@ void parkFxOffline(MediaTrack* tr) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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).
|
||||
// Restores per-FX offline from the snapshot, bounds-checked against the live
|
||||
// FX count (prune-safe if the chain changed while parked).
|
||||
//
|
||||
// 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.
|
||||
// HAZARD (open, tracked in docs/TODO.md): this remaps by slot INDEX, not
|
||||
// plugin identity. If the FX chain reshuffled while parked, snapshot slot k
|
||||
// restores onto whatever plugin now occupies slot k. Accepted for now;
|
||||
// identity-based reconciliation is future hardening.
|
||||
void restoreFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& fxOffline) {
|
||||
int fxCount = TrackFX_GetCount(tr);
|
||||
for (const FxOfflineOp& op : fxOffline) {
|
||||
@@ -190,18 +163,14 @@ void restoreFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& fxOffline)
|
||||
}
|
||||
}
|
||||
|
||||
// -- 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:<mode>"). 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.
|
||||
// Managed-lane application: the pure planner keys LanePlayOps by the lane's
|
||||
// DURABLE name; REAPER's C_LANEPLAYS:N is keyed by current ordinal, which
|
||||
// renumbers on reorder. So every write here re-resolves durable key -> current
|
||||
// ordinal first. A lane whose name lacks the managed prefix never enters this
|
||||
// map and so 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).
|
||||
// Lane `laneIdx`'s durable name (P_LANENAME:n) on `tr`, or empty if unnamed /
|
||||
// unavailable (non-fixed-lane track).
|
||||
std::string laneName(MediaTrack* tr, int laneIdx) {
|
||||
char parm[32];
|
||||
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
||||
@@ -210,9 +179,8 @@ std::string laneName(MediaTrack* tr, int laneIdx) {
|
||||
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.
|
||||
// Managed lane durable key -> current ordinal on `tr`. Manual lanes are
|
||||
// omitted, so a key absent from the map must not be driven.
|
||||
std::map<std::string, int> managedLaneOrdinals(MediaTrack* tr) {
|
||||
std::map<std::string, int> byKey;
|
||||
const int numLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||
@@ -223,37 +191,25 @@ std::map<std::string, int> managedLaneOrdinals(MediaTrack* tr) {
|
||||
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.)
|
||||
// Track-side C_LANEPLAYS:N alone hides+silences every item on lane N (SDK:
|
||||
// item-side C_LANEPLAYS is read-only, so no per-item write exists or is
|
||||
// needed). B_FIXEDLANE_HIDDEN is also read-only — hide/show follows from
|
||||
// C_LANEPLAYS=0/1, never written directly.
|
||||
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<double>(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.
|
||||
// Groups ops by track, reconciles each op's durable laneKey to the track's
|
||||
// current ordinal (a stale/renamed/deleted key is pruned, never mis-driven),
|
||||
// enables fixed-lane mode on any track carrying a managed lane, and drives
|
||||
// C_LANEPLAYS. UpdateTimeline() is the caller's job when this returns true
|
||||
// (SDK: required after an I_FREEMODE change).
|
||||
bool applyLaneOps(const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid,
|
||||
const std::vector<LanePlayOp>& lanes) {
|
||||
if (lanes.empty()) return false;
|
||||
|
||||
// Group op indices by track guid so we read each track's lane map once.
|
||||
std::map<std::string, std::vector<const LanePlayOp*>> byTrack;
|
||||
for (const LanePlayOp& op : lanes) byTrack[op.trackGuid].push_back(&op);
|
||||
|
||||
@@ -262,22 +218,18 @@ bool applyLaneOps(const std::vector<std::pair<std::string, MediaTrack*>>& handle
|
||||
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.
|
||||
// Every track reaching here already owns a managed lane (planToggle
|
||||
// only emits ops for managed lanes), so re-asserting fixed-lane mode
|
||||
// is always a tool-driven (re)split — never a user's untouched
|
||||
// manual-fixed-lane track — and gets the same transparent display.
|
||||
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||
if (freeMode != kFreeModeFixedLanes) {
|
||||
SetMediaTrackInfo_Value(tr, "I_FREEMODE",
|
||||
static_cast<double>(kFreeModeFixedLanes));
|
||||
applyTransparentLaneDisplay(tr); // tool-managed track ⇒ read like a normal track
|
||||
applyTransparentLaneDisplay(tr);
|
||||
touchedFreeMode = true;
|
||||
}
|
||||
|
||||
// Reconcile durable keys -> current ordinals on THIS track, then drive each op.
|
||||
const std::map<std::string, int> ordinals = managedLaneOrdinals(tr);
|
||||
for (const LanePlayOp* op : ops) {
|
||||
auto it = ordinals.find(op->laneKey);
|
||||
@@ -288,20 +240,12 @@ bool applyLaneOps(const std::vector<std::pair<std::string, MediaTrack*>>& handle
|
||||
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.
|
||||
// Managed-lane minting: the DECISION (which tracks split, which lanes, which
|
||||
// item goes where) is planLaneMinting; this shell only reads live per-item
|
||||
// mode+lane state, calls it, and applies the resulting 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).
|
||||
// Maps every item GUID on `tr` to its handle in one pass (avoids a per-item
|
||||
// re-scan in the assign loop).
|
||||
std::map<std::string, MediaItem*> itemHandlesByGuid(MediaTrack* tr) {
|
||||
std::map<std::string, MediaItem*> byGuid;
|
||||
const int itemCount = CountTrackMediaItems(tr);
|
||||
@@ -314,23 +258,18 @@ std::map<std::string, MediaItem*> itemHandlesByGuid(MediaTrack* tr) {
|
||||
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.
|
||||
// An untagged item is Arrange by default (mirrors leafBelongsToMode). A
|
||||
// show-both/multi-mode item resolves to its first mode id — unusual for lane
|
||||
// content, and any one mode is sufficient for the decision.
|
||||
std::string itemModeFromMembership(const ViewModeModel& model, const std::string& itemGuid) {
|
||||
const std::set<std::string> modes = model.membership().modesOf(itemGuid);
|
||||
if (modes.empty()) return kArrangeModeId; // untagged ⇒ Arrange default
|
||||
if (modes.empty()) return kArrangeModeId;
|
||||
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.
|
||||
// Builds the per-track LaneItem picture the pure decision consumes. Manual-
|
||||
// lane reads are skipped on a non-fixed-lane track (isOnManualLane is false
|
||||
// there regardless of name).
|
||||
std::vector<LaneTrack> readLaneTracks(
|
||||
const ViewModeModel& model,
|
||||
const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
||||
@@ -353,9 +292,6 @@ std::vector<LaneTrack> readLaneTracks(
|
||||
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));
|
||||
@@ -365,12 +301,9 @@ std::vector<LaneTrack> readLaneTracks(
|
||||
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".)
|
||||
// Idempotent: writes I_FIXEDLANE only when it differs from the item's current
|
||||
// lane. Non-destructive — only this reversible flag is written, never a move
|
||||
// in time or across tracks.
|
||||
bool assignItemToLane(MediaTrack* tr, MediaItem* it, int laneOrdinal) {
|
||||
const int current = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
||||
if (current == laneOrdinal) return false; // already there — no-op
|
||||
@@ -378,22 +311,16 @@ bool assignItemToLane(MediaTrack* tr, MediaItem* it, int 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).
|
||||
// Applies the pure LaneMintPlan. Returns true if any project write actually
|
||||
// changed state (⇒ 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.
|
||||
// The plan only ever names managed-prefixed lanes and only ever assigns
|
||||
// managed-eligible items; I_NUMFIXEDLANES is only ever GROWN, never shrunk,
|
||||
// so a user's existing manual lanes are never renamed or reassigned.
|
||||
bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
||||
const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
||||
bool changed = false;
|
||||
|
||||
// Group mints + assigns by track so each track is set up once.
|
||||
std::map<std::string, std::vector<const LaneMint*>> mintsByTrack;
|
||||
for (const LaneMint& m : plan.mints) mintsByTrack[m.trackGuid].push_back(&m);
|
||||
std::map<std::string, std::vector<const LaneAssign*>> assignsByTrack;
|
||||
@@ -403,37 +330,26 @@ bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
||||
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.
|
||||
// A track not already in fixed-lane mode is one the tool is splitting
|
||||
// now, so it owns the display; a track already at I_FREEMODE==2 (the
|
||||
// user's own, or a prior tool run) skips this and keeps its display prefs.
|
||||
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||
if (freeMode != kFreeModeFixedLanes) {
|
||||
SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast<double>(kFreeModeFixedLanes));
|
||||
applyTransparentLaneDisplay(tr); // tool-split track ⇒ read like a normal track
|
||||
applyTransparentLaneDisplay(tr);
|
||||
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.
|
||||
// Grow-only: a track may already carry the user's manual lanes, so the
|
||||
// lane count only ever increases; managed lanes occupy the tail ordinals.
|
||||
int laneCount = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||
|
||||
// Which managed keys are already present on this track (durable-name reconcile).
|
||||
std::map<std::string, int> 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<double>(laneCount));
|
||||
char parm[32];
|
||||
@@ -445,10 +361,6 @@ bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
||||
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<std::string, int> ordinals = managedLaneOrdinals(tr);
|
||||
const std::map<std::string, MediaItem*> itemsByGuid = itemHandlesByGuid(tr);
|
||||
for (const LaneAssign* a : assignsByTrack[split.trackGuid]) {
|
||||
@@ -465,21 +377,18 @@ bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
||||
} // 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;
|
||||
return false; // reject before touching the project — no partial apply
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
||||
std::vector<TrackFolderEntry> 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.
|
||||
// Prune snapshots 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<std::string> liveGuids;
|
||||
for (const auto& kv : handleByGuid) liveGuids.insert(kv.first);
|
||||
model.reconcile(liveGuids);
|
||||
@@ -488,31 +397,25 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
|
||||
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.
|
||||
// PARK: snapshot before mutating, store into the model, then apply.
|
||||
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;
|
||||
if (tp.flags.empty()) continue; // every op in a TrackPlan targets one track
|
||||
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.
|
||||
// Snapshot ONCE, at first park: a snapshot already present means the
|
||||
// track is still parked from a prior apply, so its live flags are the
|
||||
// parked values — recapturing would overwrite the true pre-park state
|
||||
// with zeros and a later restore would hide it for good. 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.
|
||||
// RESTORE: apply verbatim, then drop the consumed snapshot.
|
||||
for (const TrackPlan& tp : plan.restore) {
|
||||
if (tp.flags.empty()) continue;
|
||||
const std::string& guid = tp.flags.front().guid;
|
||||
@@ -524,21 +427,13 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
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.
|
||||
// MANAGED LANES: drive C_LANEPLAYS so the active mode's lane plays+shows
|
||||
// and every other managed lane is silenced+hidden. Empty for a D1-only
|
||||
// project, leaving that behavior byte-identical.
|
||||
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.
|
||||
// PARENT VISIBILITY (never parked): recomputed every toggle, never
|
||||
// snapshotted. Only the two visibility flags — never mainSend/FX on a parent.
|
||||
std::set<std::string> visible = model.visibleTracks(tree, targetModeId);
|
||||
for (const FolderNode& node : tree.nodes) {
|
||||
if (!node.isParent) continue;
|
||||
@@ -549,10 +444,8 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
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.
|
||||
// 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 " +
|
||||
@@ -560,18 +453,14 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
|
||||
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.
|
||||
// Force REAPER to rebuild the TCP/MCP now rather than on the next user
|
||||
// interaction: TrackList_AdjustWindows(false) does the full relayout owed
|
||||
// when tracks appear/disappear; UpdateArrange() repaints.
|
||||
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.
|
||||
// UpdateTimeline() is owed only when a track was actually toggled into
|
||||
// fixed lanes this apply (SDK requirement for I_FREEMODE changes).
|
||||
if (laneModeChanged) UpdateTimeline();
|
||||
|
||||
Undo_EndBlock2(proj, undoLabel.c_str(), -1);
|
||||
@@ -581,63 +470,41 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
||||
std::vector<TrackFolderEntry> 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.
|
||||
// The tree is needed to detect a content-bearing folder derived-visible in
|
||||
// >1 mode, exactly as applyMode builds it.
|
||||
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<LaneTrack> 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 REAPER write was already satisfied. 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.
|
||||
// Plan was non-empty but every write was already satisfied — discard
|
||||
// the empty undo point rather than flooding history every detect tick.
|
||||
Undo_EndBlock2(proj, "", 0);
|
||||
|
||||
// BUT the arrange still needs a redraw. On the detect-tick caller (bankPanelRefresh)
|
||||
// mintManagedLanes runs only when this tick just tagged new content, and a NON-EMPTY
|
||||
// plan means that content sits on a managed-split track. The idempotent no-op path is
|
||||
// reached when a freshly-inserted item ALREADY landed on the active mode's playing
|
||||
// lane (REAPER places a new item on the playing lane; the active mode's lane IS the
|
||||
// playing lane, so assignItemToLane sees I_FIXEDLANE unchanged and writes nothing).
|
||||
// The item is correctly placed and confined, but the arrange was never told to
|
||||
// repaint it onto the lane — so it stayed invisible until a manual mode toggle forced
|
||||
// applyMode's refresh. Force the redraw here so the item appears immediately without a
|
||||
// toggle. UpdateArrange() only repaints (no I_FREEMODE transition happened on this
|
||||
// path, so UpdateTimeline is not owed); it is NOT a project mutation, so it stays
|
||||
// outside the undo block and adds no history entry. On the action caller (doMoveItems)
|
||||
// this is a harmless repaint immediately before its own reapplyActiveMode() refresh.
|
||||
// The arrange still needs a redraw: this no-op path is reached when a
|
||||
// freshly-inserted item already landed on the active mode's playing
|
||||
// lane (REAPER places new items on the playing lane), so
|
||||
// assignItemToLane wrote nothing even though the item needs to appear
|
||||
// there now. UpdateArrange() alone (no I_FREEMODE change happened, so
|
||||
// UpdateTimeline isn't owed) is a repaint, not a mutation — stays
|
||||
// outside the undo block.
|
||||
UpdateArrange();
|
||||
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.
|
||||
// Reapply the active mode's lane visibility so freshly-minted lanes take
|
||||
// their play/show state immediately. applyMode is deliberately NOT reused
|
||||
// here — it would re-park/restore whole tracks and recompute parent
|
||||
// visibility, which a lane-only mint must not touch.
|
||||
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();
|
||||
UpdateTimeline(); // a split happened this call — refresh is owed
|
||||
UpdateArrange();
|
||||
|
||||
Undo_EndBlock2(proj, "ReaSampler: separate cross-mode content into lanes", -1);
|
||||
@@ -648,12 +515,9 @@ void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||
std::vector<std::pair<std::string, MediaTrack*>> 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.
|
||||
// Pure read of REAPER state (no lane created, no I_FREEMODE/I_NUMFIXEDLANES/
|
||||
// I_FIXEDLANE written) plus an ownership-index write, recovering managed
|
||||
// classification from the durable name. An unprefixed lane is left alone.
|
||||
for (const auto& [guid, tr] : handleByGuid) {
|
||||
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||
if (freeMode != kFreeModeFixedLanes) continue; // no fixed lanes ⇒ nothing managed
|
||||
@@ -666,15 +530,12 @@ void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||
std::optional<std::string> 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.
|
||||
// A mode id encoded in the name may no longer be registered (e.g.
|
||||
// removed since the project was saved). Recording it managed would
|
||||
// make the toggle planner drive a lane keyed to a mode that can
|
||||
// never be active — permanently silenced, orphaning its items. So
|
||||
// skip: the lane stays off the index (manual-by-default) but keeps
|
||||
// its name, so a later re-registration of the mode heals cleanly.
|
||||
if (!model.modes().contains(*mode)) continue;
|
||||
model.lanes().setManaged(guid, *key, *mode);
|
||||
}
|
||||
|
||||
+20
-78
@@ -1,95 +1,37 @@
|
||||
#pragma once
|
||||
// view — the REAPER-facing shell of the Design View feature (Phase D2). It is the
|
||||
// mirror of the capture shell: the ViewModeModel (pure, D1) holds the mode/
|
||||
// membership/snapshot state and emits the toggle plan; this shell reads the live
|
||||
// project's folder tree, snapshots the tracks it is about to park, runs the model's
|
||||
// planner, and applies the resulting flag + per-FX-offline writes to REAPER.
|
||||
//
|
||||
// It includes view_mode_model (pure) but NO REAPER headers — the .cpp is the one
|
||||
// REAPER-facing translation unit (CLAUDE.md §contract: only main.cpp defines the
|
||||
// API pointers; every other .cpp gets them extern). Callers (persist, actions)
|
||||
// depend on this seam without dragging the SDK into their include sites.
|
||||
//
|
||||
// Hard invariants this shell enforces (CONTEXT.md §Design View, precision
|
||||
// invariants) — verified in self-review, never crossed:
|
||||
// * Never touches the master track's visibility (SDK forbids B_SHOWINTCP/
|
||||
// B_SHOWINMIXER on master); the master is never a node in the tree.
|
||||
// * Never reads or writes B_MUTE / I_SOLO on any track.
|
||||
// * Manages ALL leaves via the mode system: an untagged leaf is an Arrange member,
|
||||
// so it is fully parked in non-Arrange modes and restored in Arrange, identically
|
||||
// to a tagged leaf. show-both is the always-visible escape; parents are
|
||||
// visibility-only (derived); the master is never touched.
|
||||
// * Snapshots every to-be-parked track's prior flags BEFORE parking, storing
|
||||
// them into the model so restore is faithful and survives a save-while-parked.
|
||||
// REAPER-facing shell of Design View (D2): reads the live folder tree, runs
|
||||
// ViewModeModel's pure planner, and applies the resulting flag / per-FX /
|
||||
// lane writes. The .cpp is the sole REAPER-facing TU here (CLAUDE.md contract:
|
||||
// only main.cpp defines the API pointers). See src/shell/view/CLAUDE.md for
|
||||
// the enforced invariants (never touch master/mute/solo, snapshot-based restore).
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "core/view/view_mode_model.h"
|
||||
|
||||
// REAPER's opaque project handle. Forward-declared to keep this header SDK-free;
|
||||
// the .cpp includes reaper_plugin_functions.h and sees the real class.
|
||||
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
|
||||
class ReaProject;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// Applies `targetModeId` to the live project `proj`:
|
||||
// 1. Reads the arrange-ordered track list, builds the FolderTree from
|
||||
// I_FOLDERDEPTH (via the pure buildFolderTree helper).
|
||||
// 2. Runs model.planToggle(tree, targetModeId).
|
||||
// 3. For each track about to be PARKED: snapshots its current B_SHOWINTCP /
|
||||
// B_SHOWINMIXER / B_MAINSEND / I_FXEN and per-FX offline state, stores the
|
||||
// snapshot into the model, THEN applies the park writes (expanding the
|
||||
// per-FX offline loop from TrackFX_GetCount, which the pure plan leaves empty).
|
||||
// 4. For each track to RESTORE: applies the plan's snapshot-sourced flag + per-FX
|
||||
// offline writes verbatim.
|
||||
// 5. For each PARENT (folder) node: drives B_SHOWINTCP / B_SHOWINMIXER to 1 if the
|
||||
// parent is in model.visibleTracks(tree, targetModeId), else 0 — derived from
|
||||
// membership, never parked/snapshotted. Only the two visibility flags.
|
||||
// 6. Sets the model's active mode to `targetModeId`.
|
||||
// All track mutations are wrapped in Undo_BeginBlock2 / Undo_EndBlock2.
|
||||
//
|
||||
// Returns false (no mutation, active mode unchanged) if `targetModeId` is not a
|
||||
// registered mode. `proj` may be nullptr to mean REAPER's current project.
|
||||
// Snapshots each about-to-park track's flags into `model`, runs planToggle,
|
||||
// applies park/restore writes plus parent visibility flags, then sets the
|
||||
// active mode. Wrapped in one Undo block. Returns false (no mutation) if
|
||||
// `targetModeId` isn't registered. `proj` == nullptr means the current project.
|
||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj);
|
||||
|
||||
// Mints managed fixed lanes for any track in `proj` that is VISIBLE IN MORE THAN ONE
|
||||
// MODE while carrying its own media, and assigns each item to its mode's managed lane
|
||||
// (Phase D2 Wave 3; visibility trigger added by the folder-media fix).
|
||||
// 1. Enumerates every track + its items; resolves each item's mode from the model's
|
||||
// membership (untagged ⇒ Arrange) and reads whether it currently sits on a MANUAL
|
||||
// lane (exempt). Builds the FolderTree (I_FOLDERDEPTH) so derived visibility counts.
|
||||
// 2. Runs the pure planLaneMinting decision (model + tree aware). A track visible in
|
||||
// exactly one mode is left whole-track-parked (D1) — NOT lane-split. A track visible
|
||||
// in >1 mode while carrying own media splits: its own items span modes, OR it is a
|
||||
// content-bearing folder derived-visible across modes. show-both tracks never split.
|
||||
// 3. For each track that must split: enables fixed-lane mode (I_FREEMODE=2), ensures
|
||||
// enough fixed lanes (I_NUMFIXEDLANES), stamps each managed lane's durable name
|
||||
// (P_LANENAME:n), records the lane MANAGED-for-its-mode in the model's ownership
|
||||
// index, and assigns each managed-eligible item to its mode's lane (I_FIXEDLANE).
|
||||
// Manual lanes and the items on them are NEVER minted-over or reassigned.
|
||||
// 4. Reapplies the active mode's lane visibility so the just-minted lanes take their
|
||||
// correct play/show state immediately (the active mode's lane plays; others hide).
|
||||
// The whole structural mutation is wrapped in ONE Undo_BeginBlock2/EndBlock2 — but only
|
||||
// when the plan is non-empty (no undo point for a tick that mints nothing).
|
||||
//
|
||||
// Returns true if any lane was minted this call (⇒ the caller may want a repaint).
|
||||
// `proj` may be nullptr to mean REAPER's current project. READ of the membership index
|
||||
// only; the sole model mutation is recording new managed-lane ownership.
|
||||
// Splits any track visible in more than one mode while carrying its own media
|
||||
// into fixed lanes (one managed lane per involved mode), assigns items, and
|
||||
// records ownership in `model`. Never touches manual lanes. Runs planLaneMinting
|
||||
// (model + tree aware); wraps the mutation in one Undo block when non-empty.
|
||||
// Returns true if any lane was minted (repaint hint). `proj` == nullptr means
|
||||
// the current project.
|
||||
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj);
|
||||
|
||||
// Reconciles the model's lane-ownership index against the live project's lanes on
|
||||
// project open (Phase D2 Wave 3). REAPER's durable P_LANENAME is the source of truth for
|
||||
// lane identity across sessions (design point #2): a lane whose name carries the managed
|
||||
// prefix is tool-managed and owned by the mode encoded in that name. This walks every
|
||||
// track's lanes and records each managed-named lane MANAGED-for-its-mode in the index —
|
||||
// self-healing a saved project's classification WITHOUT re-minting (it never creates a
|
||||
// lane, changes I_FREEMODE/I_NUMFIXEDLANES, or reassigns an item) and WITHOUT mass-
|
||||
// tagging (it never touches membership). A lane without the managed prefix is left
|
||||
// untouched (manual by default). Reload's active-mode lane visibility is then reapplied
|
||||
// by the caller's applyMode, mirroring D1's reapply-on-open.
|
||||
//
|
||||
// `proj` may be nullptr to mean REAPER's current project. The only model mutation is
|
||||
// recording managed ownership recovered from durable lane names.
|
||||
// Recovers managed-lane ownership from durable P_LANENAME on project open —
|
||||
// self-healing, without minting/reassigning anything and without touching
|
||||
// membership. A lane without the managed prefix is left untouched (manual).
|
||||
// `proj` == nullptr means the current project.
|
||||
void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
Reference in New Issue
Block a user