fix(view): park untagged leaves in non-Arrange modes; refresh TCP/MCP on apply

planToggle now enumerates all leaves in the FolderTree instead of only tagged
membership entries, so untagged (Arrange-member) tracks park in Design and
restore in Arrange. applyMode calls TrackList_AdjustWindows + UpdateArrange so
changes render immediately. Correct the stale "tagged-only" invariant comments.
This commit is contained in:
2026-07-23 04:59:50 -04:00
parent 60add4afc7
commit 4bf394b89c
5 changed files with 130 additions and 22 deletions
+10
View File
@@ -27,6 +27,8 @@
#define REAPERAPI_WANT_TrackFX_SetOffline #define REAPERAPI_WANT_TrackFX_SetOffline
#define REAPERAPI_WANT_Undo_BeginBlock2 #define REAPERAPI_WANT_Undo_BeginBlock2
#define REAPERAPI_WANT_Undo_EndBlock2 #define REAPERAPI_WANT_Undo_EndBlock2
#define REAPERAPI_WANT_TrackList_AdjustWindows
#define REAPERAPI_WANT_UpdateArrange
#include "reaper_plugin_functions.h" #include "reaper_plugin_functions.h"
namespace reasampler { namespace reasampler {
@@ -191,6 +193,14 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
model.setActiveMode(targetModeId); 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();
Undo_EndBlock2(proj, "ReaSampler: apply Design View mode", -1); Undo_EndBlock2(proj, "ReaSampler: apply Design View mode", -1);
return true; return true;
} }
+4 -2
View File
@@ -15,8 +15,10 @@
// * Never touches the master track's visibility (SDK forbids B_SHOWINTCP/ // * Never touches the master track's visibility (SDK forbids B_SHOWINTCP/
// B_SHOWINMIXER on master); the master is never a node in the tree. // B_SHOWINMIXER on master); the master is never a node in the tree.
// * Never reads or writes B_MUTE / I_SOLO on any track. // * Never reads or writes B_MUTE / I_SOLO on any track.
// * Acts only on tracks the model owns (tagged leaves the planner names) plus // * Manages ALL leaves via the mode system: an untagged leaf is an Arrange member,
// derived parents' visibility — never an untagged track's owned flags. // 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 // * 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. // them into the model so restore is faithful and survives a save-while-parked.
+17 -13
View File
@@ -204,15 +204,18 @@ std::set<std::string> ViewModeModel::visibleTracks(const FolderTree& tree,
TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string& targetMode) const { TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string& targetMode) const {
TogglePlan plan; TogglePlan plan;
// Index the tree so we can classify each tagged GUID (leaf vs parent vs stale). // The mode system manages EVERY leaf, not just tagged ones. An untagged leaf is
std::map<std::string, const FolderNode*> byGuid; // an Arrange member (leafBelongsToMode resolves that), so it must park when the
for (const auto& node : tree.nodes) byGuid[node.guid] = &node; // target mode is not Arrange and restore when it is — the same full park/restore
// a tagged leaf gets. Enumerating the FolderTree (not membership_.all()) is what
for (const auto& [guid, m] : membership_.all()) { // brings untagged leaves — which are absent from the membership index — under
auto it = byGuid.find(guid); // management. Parents are visibility-only (handled by visibleTracks + the shell's
if (it == byGuid.end()) continue; // stale GUID: prune-safe, ignore // parent-visibility pass) and show-both leaves are the always-visible escape;
if (it->second->isParent) continue; // parents are derived, never parked // neither is ever parked.
if (m.showBoth) continue; // show-both leaves are never parked for (const auto& node : tree.nodes) {
if (node.isParent) continue; // parents are derived, never parked
const std::string& guid = node.guid;
if (membership_.isShowBoth(guid)) continue; // show-both leaves are never parked
const bool active = leafBelongsToMode(guid, targetMode); const bool active = leafBelongsToMode(guid, targetMode);
if (active) { if (active) {
@@ -221,10 +224,11 @@ TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string&
if (const TrackSnapshot* snap = snapshot(guid)) if (const TrackSnapshot* snap = snapshot(guid))
plan.restore.push_back(makeRestorePlan(guid, *snap)); plan.restore.push_back(makeRestorePlan(guid, *snap));
} else { } else {
// Inactive tagged leaf ⇒ park. fxOffline is intentionally empty here: // Inactive leaf (tagged into another mode, or untagged in a non-Arrange
// the D2 shell expands per-FX offline writes using TrackFX_GetCount. // mode) ⇒ park. fxOffline is intentionally empty here: the D2 shell
// The pure model has no access to REAPER FX counts at plan time; // expands per-FX offline writes using TrackFX_GetCount. The pure model
// makeParkPlan(guid, 0) emits only the scalar flags as a result. // has no access to REAPER FX counts at plan time; makeParkPlan(guid, 0)
// emits only the scalar flags as a result.
plan.park.push_back(makeParkPlan(guid, /*fxCount=*/0)); plan.park.push_back(makeParkPlan(guid, /*fxCount=*/0));
} }
} }
+11 -7
View File
@@ -232,8 +232,9 @@ struct TrackPlan {
// The plan for a whole toggle to a target mode: which tracks to park, and which to // The plan for a whole toggle to a target mode: which tracks to park, and which to
// restore from their snapshots. Parents and show-both leaves never appear here — // restore from their snapshots. Parents and show-both leaves never appear here —
// they are derived-visible and never parked (visibility is answered separately by // they are derived-visible and never parked (visibility is answered separately by
// visibleTracks). Untagged tracks never appear either (the tool owns only what it // visibleTracks). Untagged LEAVES DO appear: an untagged leaf is an Arrange member,
// tagged). // so it parks in every non-Arrange mode and restores in Arrange — the mode system
// manages all leaves, not only tagged ones.
struct TogglePlan { struct TogglePlan {
std::vector<TrackPlan> park; // inactive leaves -> parked (fixed zeros) std::vector<TrackPlan> park; // inactive leaves -> parked (fixed zeros)
std::vector<TrackPlan> restore; // active leaves returning -> snapshot values std::vector<TrackPlan> restore; // active leaves returning -> snapshot values
@@ -278,11 +279,14 @@ public:
std::set<std::string> visibleTracks(const FolderTree& tree, std::set<std::string> visibleTracks(const FolderTree& tree,
const std::string& modeId) const; const std::string& modeId) const;
// Plans a toggle to `targetMode` against the current tree. Inactive tagged // Plans a toggle to `targetMode` by enumerating EVERY leaf in the supplied tree.
// leaves (not show-both, not derived-visible-only) are parked with fixed zeros; // A leaf inactive in the target mode — tagged into another mode, or untagged and
// leaves that become active AND have a stored snapshot are restored from it. // the target isn't Arrange — is parked with fixed zeros; a leaf that becomes
// Parents, show-both leaves, the master, and untagged tracks are never parked. // active AND has a stored snapshot is restored from it. Parents (visibility-only)
// Unknown/stale membership GUIDs absent from the tree are ignored (prune-safe). // and show-both leaves (always visible) are never parked; the master is not in
// the tree. Untagged leaves ARE managed: they are Arrange members, so they park
// in non-Arrange modes and restore in Arrange. Tree membership is the enumeration
// source, so stale membership GUIDs absent from the tree are naturally ignored.
// //
// Note: park plans emitted here have an empty fxOffline vector. The D2 shell // Note: park plans emitted here have an empty fxOffline vector. The D2 shell
// expands per-FX offline writes using TrackFX_GetCount — the pure model has no // expands per-FX offline writes using TrackFX_GetCount — the pure model has no
+88
View File
@@ -383,6 +383,92 @@ static void testPlanToggleParkHasEmptyFxOffline() {
CHECK(plan.park[0].flags.size() == 4); CHECK(plan.park[0].flags.size() == 4);
} }
// -- 7b. Untagged leaves are managed by the mode system ----------------------
//
// The core semantic fix: an untagged leaf is an Arrange member. planToggle must
// enumerate EVERY leaf in the tree (not just membership_.all()), so an untagged
// leaf — absent from the membership index — parks in every non-Arrange mode and
// restores in Arrange, identically to a tagged leaf. Parents and show-both leaves
// remain never-parked. Tagged-leaf behavior is unchanged.
static void testUntaggedLeavesManagedByModeSystem() {
ViewModeModel vm;
// A tree of leaves NONE of which are in the membership index (all untagged),
// plus a parent folder and a show-both leaf to prove they stay untouched.
FolderTree tree;
tree.nodes.push_back(FolderNode{"{P}", "", /*isParent=*/true});
tree.nodes.push_back(FolderNode{"{U1}", "{P}", false}); // untagged leaf
tree.nodes.push_back(FolderNode{"{U2}", "{P}", false}); // untagged leaf
tree.nodes.push_back(FolderNode{"{SB}", "", false}); // show-both leaf
vm.membership().setShowBoth("{SB}", true);
// (iii) Enumeration covers leaves absent from the membership index: {U1}/{U2}
// are NOT in membership_.all(), yet the planner reaches them.
CHECK(vm.membership().query("{U1}") == nullptr);
CHECK(vm.membership().query("{U2}") == nullptr);
// (i) Toggling to Design (non-Arrange): every untagged leaf is parked.
auto toDesign = vm.planToggle(tree, kDesignModeId);
CHECK(parkTargets(toDesign, "{U1}"));
CHECK(parkTargets(toDesign, "{U2}"));
// (iv) The parent and the show-both leaf are NEVER parked, in either mode.
CHECK(!parkTargets(toDesign, "{P}"));
CHECK(!parkTargets(toDesign, "{SB}"));
// (ii) Toggling to Arrange: the untagged leaves are Arrange members ⇒ active and
// NOT parked. (No snapshot stored yet ⇒ no restore op either; just not parked.)
auto toArrange = vm.planToggle(tree, kArrangeModeId);
CHECK(!parkTargets(toArrange, "{U1}"));
CHECK(!parkTargets(toArrange, "{U2}"));
CHECK(restoreFor(toArrange, "{U1}") == nullptr);
CHECK(!parkTargets(toArrange, "{P}"));
CHECK(!parkTargets(toArrange, "{SB}"));
// (vi) Restore-from-snapshot fidelity for a previously-parked UNTAGGED leaf:
// an untagged leaf parked while in Design carries a snapshot; toggling back to
// Arrange restores it from that snapshot verbatim, never a hardcoded default.
TrackSnapshot snap;
snap.showInTcp = 1; snap.showInMixer = 1; snap.mainSend = 0; snap.fxEnable = 1;
snap.fxOffline = {0, 1};
vm.storeSnapshot("{U1}", snap); // as the shell would, before parking it in Design
auto backToArrange = vm.planToggle(tree, kArrangeModeId);
const TrackPlan* r = restoreFor(backToArrange, "{U1}");
CHECK(r != nullptr);
if (r) {
CHECK(flagValue(*r, Flag::ShowInTcp) == 1);
CHECK(flagValue(*r, Flag::ShowInMixer) == 1);
CHECK(flagValue(*r, Flag::MainSend) == 0); // captured 0 comes back 0
CHECK(flagValue(*r, Flag::FxEnable) == 1);
CHECK(r->fxOffline.size() == 2);
CHECK(r->fxOffline[0].offline == false);
CHECK(r->fxOffline[1].offline == true);
}
}
// (v) Tagged-leaf park/restore behavior is unchanged after the untagged fix: a leaf
// tagged Design parks in Arrange and is active (not parked) in Design, and a mix of
// tagged + untagged leaves each land on the correct side of the toggle.
static void testTaggedLeafBehaviorUnchangedWithUntagged() {
ViewModeModel vm;
FolderTree tree;
tree.nodes.push_back(FolderNode{"{DES}", "", false}); // tagged into Design
tree.nodes.push_back(FolderNode{"{UNT}", "", false}); // untagged ⇒ Arrange
vm.membership().tag("{DES}", kDesignModeId);
// In Design: {DES} active (not parked); {UNT} inactive ⇒ parked.
auto design = vm.planToggle(tree, kDesignModeId);
CHECK(!parkTargets(design, "{DES}"));
CHECK(parkTargets(design, "{UNT}"));
// In Arrange: {DES} inactive ⇒ parked; {UNT} active (not parked).
auto arrange = vm.planToggle(tree, kArrangeModeId);
CHECK(parkTargets(arrange, "{DES}"));
CHECK(!parkTargets(arrange, "{UNT}"));
}
// -- 8. nextModeId cycle (D4 toggle helper) ---------------------------------- // -- 8. nextModeId cycle (D4 toggle helper) ----------------------------------
static void testNextModeIdCycles() { static void testNextModeIdCycles() {
@@ -417,6 +503,8 @@ int main() {
testEmptyModelRoundTrip(); testEmptyModelRoundTrip();
testMalformedJson(); testMalformedJson();
testPlanToggleParkHasEmptyFxOffline(); testPlanToggleParkHasEmptyFxOffline();
testUntaggedLeavesManagedByModeSystem();
testTaggedLeafBehaviorUnchangedWithUntagged();
testNextModeIdCycles(); testNextModeIdCycles();
if (g_fail == 0) std::printf("All tests passed.\n"); if (g_fail == 0) std::printf("All tests passed.\n");