diff --git a/src/view.cpp b/src/view.cpp index 91d113e..04a68fe 100644 --- a/src/view.cpp +++ b/src/view.cpp @@ -27,6 +27,8 @@ #define REAPERAPI_WANT_TrackFX_SetOffline #define REAPERAPI_WANT_Undo_BeginBlock2 #define REAPERAPI_WANT_Undo_EndBlock2 +#define REAPERAPI_WANT_TrackList_AdjustWindows +#define REAPERAPI_WANT_UpdateArrange #include "reaper_plugin_functions.h" namespace reasampler { @@ -191,6 +193,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. + TrackList_AdjustWindows(false); + UpdateArrange(); + Undo_EndBlock2(proj, "ReaSampler: apply Design View mode", -1); return true; } diff --git a/src/view.h b/src/view.h index 1b1d248..46b7fa1 100644 --- a/src/view.h +++ b/src/view.h @@ -15,8 +15,10 @@ // * 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. -// * Acts only on tracks the model owns (tagged leaves the planner names) plus -// derived parents' visibility — never an untagged track's owned flags. +// * 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. diff --git a/src/view_mode_model.cpp b/src/view_mode_model.cpp index 77d5bad..3fb688c 100644 --- a/src/view_mode_model.cpp +++ b/src/view_mode_model.cpp @@ -204,15 +204,18 @@ std::set ViewModeModel::visibleTracks(const FolderTree& tree, TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string& targetMode) const { TogglePlan plan; - // Index the tree so we can classify each tagged GUID (leaf vs parent vs stale). - std::map byGuid; - for (const auto& node : tree.nodes) byGuid[node.guid] = &node; - - for (const auto& [guid, m] : membership_.all()) { - auto it = byGuid.find(guid); - if (it == byGuid.end()) continue; // stale GUID: prune-safe, ignore - if (it->second->isParent) continue; // parents are derived, never parked - if (m.showBoth) continue; // show-both leaves are never parked + // The mode system manages EVERY leaf, not just tagged ones. An untagged leaf is + // an Arrange member (leafBelongsToMode resolves that), so it must park when the + // 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 + // brings untagged leaves — which are absent from the membership index — under + // management. Parents are visibility-only (handled by visibleTracks + the shell's + // parent-visibility pass) and show-both leaves are the always-visible escape; + // neither is ever 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); if (active) { @@ -221,10 +224,11 @@ TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string& if (const TrackSnapshot* snap = snapshot(guid)) plan.restore.push_back(makeRestorePlan(guid, *snap)); } else { - // Inactive tagged leaf ⇒ park. fxOffline is intentionally empty here: - // the D2 shell expands per-FX offline writes using TrackFX_GetCount. - // The pure model has no access to REAPER FX counts at plan time; - // makeParkPlan(guid, 0) emits only the scalar flags as a result. + // Inactive leaf (tagged into another mode, or untagged in a non-Arrange + // mode) ⇒ park. fxOffline is intentionally empty here: the D2 shell + // expands per-FX offline writes using TrackFX_GetCount. The pure model + // 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)); } } diff --git a/src/view_mode_model.h b/src/view_mode_model.h index 353eea3..1c885b6 100644 --- a/src/view_mode_model.h +++ b/src/view_mode_model.h @@ -232,8 +232,9 @@ struct TrackPlan { // 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 — // they are derived-visible and never parked (visibility is answered separately by -// visibleTracks). Untagged tracks never appear either (the tool owns only what it -// tagged). +// visibleTracks). Untagged LEAVES DO appear: an untagged leaf is an Arrange member, +// so it parks in every non-Arrange mode and restores in Arrange — the mode system +// manages all leaves, not only tagged ones. struct TogglePlan { std::vector park; // inactive leaves -> parked (fixed zeros) std::vector restore; // active leaves returning -> snapshot values @@ -278,11 +279,14 @@ public: std::set visibleTracks(const FolderTree& tree, const std::string& modeId) const; - // Plans a toggle to `targetMode` against the current tree. Inactive tagged - // leaves (not show-both, not derived-visible-only) are parked with fixed zeros; - // leaves that become active AND have a stored snapshot are restored from it. - // Parents, show-both leaves, the master, and untagged tracks are never parked. - // Unknown/stale membership GUIDs absent from the tree are ignored (prune-safe). + // Plans a toggle to `targetMode` by enumerating EVERY leaf in the supplied tree. + // A leaf inactive in the target mode — tagged into another mode, or untagged and + // the target isn't Arrange — is parked with fixed zeros; a leaf that becomes + // active AND has a stored snapshot is restored from it. Parents (visibility-only) + // 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 // expands per-FX offline writes using TrackFX_GetCount — the pure model has no diff --git a/tests/test_view_mode_model.cpp b/tests/test_view_mode_model.cpp index d8cf5d8..8630d9f 100644 --- a/tests/test_view_mode_model.cpp +++ b/tests/test_view_mode_model.cpp @@ -383,6 +383,92 @@ static void testPlanToggleParkHasEmptyFxOffline() { 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) ---------------------------------- static void testNextModeIdCycles() { @@ -417,6 +503,8 @@ int main() { testEmptyModelRoundTrip(); testMalformedJson(); testPlanToggleParkHasEmptyFxOffline(); + testUntaggedLeavesManagedByModeSystem(); + testTaggedLeafBehaviorUnchangedWithUntagged(); testNextModeIdCycles(); if (g_fail == 0) std::printf("All tests passed.\n");