From 8d08279bdb217dee8aca475dcccbf159baa48de5 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Thu, 23 Jul 2026 06:17:34 -0400 Subject: [PATCH 1/2] Fix parent visibility: parent visible by own membership OR derived from children visibleTracks pass 1 now evaluates every node (parent or leaf) by its own membership, so an untagged folder carrying its own FX/media shows in Arrange (its default) as well as any mode derived from its children. Adds case-matrix tests; updates the nested-Arrange assertion to the corrected rule. --- src/view_mode_model.cpp | 25 +++++--- src/view_mode_model.h | 12 ++-- tests/test_view_mode_model.cpp | 112 ++++++++++++++++++++++++++++++++- 3 files changed, 134 insertions(+), 15 deletions(-) diff --git a/src/view_mode_model.cpp b/src/view_mode_model.cpp index 3fb688c..bcd5244 100644 --- a/src/view_mode_model.cpp +++ b/src/view_mode_model.cpp @@ -172,24 +172,31 @@ std::set ViewModeModel::visibleTracks(const FolderTree& tree, const std::string& modeId) const { std::set visible; - // Pass 1: every leaf that belongs to the mode is visible. + // Pass 1: every node — leaf OR parent — that belongs to the mode by its OWN + // membership is visible. For a leaf this is the tagged/show-both/untagged-Arrange + // rule; for a parent it means an untagged folder (which carries its own FX/media + // and defaults to Arrange) shows in Arrange even when none of its children do. + // Parents ALSO become visible in pass 2 by derivation from a visible descendant; + // the two rules are OR'd, so an untagged folder of all-Design leaves shows in both + // Arrange (own default) and Design (derived). for (const auto& node : tree.nodes) { - if (node.isParent) continue; // parents derived in pass 2 if (leafBelongsToMode(node.guid, modeId)) visible.insert(node.guid); } - // Pass 2: a parent is visible if any descendant leaf is visible. Walk each - // visible leaf up its parent chain and mark ancestors. Parent chains are read - // from the supplied tree only (no REAPER access). A cycle-guard bounds the walk - // in case a malformed tree links a node to itself. + // Pass 2: a parent is also visible if any descendant is visible. Walk each + // currently-visible node up its parent chain and mark ancestors. Seeding from the + // full pass-1 set means a parent made visible by its own membership propagates its + // visibility up the remaining ancestors too. Parent chains are read from the + // supplied tree only (no REAPER access). A cycle-guard bounds the walk in case a + // malformed tree links a node to itself. std::map parentOf; for (const auto& node : tree.nodes) parentOf[node.guid] = node.parentGuid; - // Snapshot the leaf-visible set so we don't re-walk parents we add mid-loop. + // Snapshot the pass-1 visible set so we don't re-walk parents we add mid-loop. const std::vector seeds(visible.begin(), visible.end()); - for (const auto& leaf : seeds) { - auto it = parentOf.find(leaf); + for (const auto& node : seeds) { + auto it = parentOf.find(node); std::size_t guard = 0; while (it != parentOf.end() && !it->second.empty() && guard++ < parentOf.size()) { const std::string& parent = it->second; diff --git a/src/view_mode_model.h b/src/view_mode_model.h index 1c885b6..28056b3 100644 --- a/src/view_mode_model.h +++ b/src/view_mode_model.h @@ -150,8 +150,9 @@ private: // -- Folder tree (INPUT, not stored) ---------------------------------------- // // The shell builds this from I_FOLDERDEPTH each time and passes it to a visibility -// query. A node is a leaf or a parent; a parent is visible in every mode any of -// its descendant leaves belongs to, and is never parked. The master track is +// query. A node is a leaf or a parent; a parent is visible in a mode if it belongs +// to that mode by its own membership OR any of its descendant leaves does, and is +// never parked. The master track is // modeled implicitly (always visible, never touched) and is NOT a node here. struct FolderNode { std::string guid; @@ -273,8 +274,11 @@ public: bool leafBelongsToMode(const std::string& guid, const std::string& modeId) const; // The set of track GUIDs visible in `modeId`, tree-aware: active leaves, - // show-both leaves, and every parent with at least one descendant leaf in the - // mode. Untagged leaves count as Arrange. Stale GUIDs in the tree are tolerated. + // show-both leaves, and every parent that EITHER belongs to the mode by its own + // membership OR has at least one descendant visible in the mode. Untagged nodes + // (leaf or folder) count as Arrange, so an untagged folder carrying its own + // FX/media shows in Arrange even when none of its children do, and additionally + // shows in a child's mode by derivation. Stale GUIDs in the tree are tolerated. // The master is not represented (always visible; the shell never touches it). std::set visibleTracks(const FolderTree& tree, const std::string& modeId) const; diff --git a/tests/test_view_mode_model.cpp b/tests/test_view_mode_model.cpp index 0b3b2be..f0a9569 100644 --- a/tests/test_view_mode_model.cpp +++ b/tests/test_view_mode_model.cpp @@ -141,9 +141,13 @@ static void testParentDerivationMultiMode() { CHECK(visibleHas(d2, "{L3}")); CHECK(visibleHas(d2, "{F2}")); CHECK(visibleHas(d2, "{G}")); - // In Arrange, none of the chain is visible (no Arrange leaf under it). + // In Arrange: the Design leaf {L3} stays hidden (leaf visibility unchanged), but + // the untagged parents {F2}/{G} are Arrange members by their own default, so they + // are visible in Arrange under the corrected own-membership OR derived rule. + // (See testParentOwnMembershipVisibility for the full case matrix.) auto a2 = vm2.visibleTracks(nested, kArrangeModeId); - CHECK(!visibleHas(a2, "{L3}") && !visibleHas(a2, "{F2}") && !visibleHas(a2, "{G}")); + CHECK(!visibleHas(a2, "{L3}")); + CHECK(visibleHas(a2, "{F2}") && visibleHas(a2, "{G}")); // A parent is NEVER parked, in either mode. auto planD = vm.planToggle(tree, kDesignModeId); @@ -152,6 +156,109 @@ static void testParentDerivationMultiMode() { CHECK(!parkTargets(planA, "{F}")); } +// -- 2b. Parent own-membership visibility (untagged folder + own FX/media) --- +// +// Corrected rule: a parent is visible in mode M if EITHER a descendant leaf is +// visible in M (existing derived rule) OR the parent belongs to M by its OWN +// membership (leafBelongsToMode on the parent's own GUID; untagged ⇒ Arrange). +// The reported case: an untagged folder whose leaves are all Design vanished in +// Arrange despite carrying its own FX/media. It must now show in Arrange (own +// default) AND Design (derived from children). Parents remain never parked. + +static void testParentOwnMembershipVisibility() { + // Case A [reported]: untagged folder {F}, all leaves Design ⇒ folder visible in + // BOTH Arrange (own default) and Design (derived from children). + { + ViewModeModel vm; + FolderTree tree; + tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true}); + tree.nodes.push_back(FolderNode{"{L1}", "{F}", false}); + tree.nodes.push_back(FolderNode{"{L2}", "{F}", false}); + vm.membership().tag("{L1}", kDesignModeId); + vm.membership().tag("{L2}", kDesignModeId); + // {F} itself untagged ⇒ Arrange member by default. + + auto arrange = vm.visibleTracks(tree, kArrangeModeId); + auto design = vm.visibleTracks(tree, kDesignModeId); + CHECK(visibleHas(arrange, "{F}")); // own default (would FAIL under derived-only rule) + CHECK(visibleHas(design, "{F}")); // derived from Design children + // Leaves appear only in Design; neither is visible in Arrange. + CHECK(visibleHas(design, "{L1}") && visibleHas(design, "{L2}")); + CHECK(!visibleHas(arrange, "{L1}") && !visibleHas(arrange, "{L2}")); + // Still never parked, in either mode. + CHECK(!parkTargets(vm.planToggle(tree, kArrangeModeId), "{F}")); + CHECK(!parkTargets(vm.planToggle(tree, kDesignModeId), "{F}")); + } + + // Case B: untagged folder, all leaves Arrange ⇒ folder visible in Arrange only. + { + ViewModeModel vm; + FolderTree tree; + tree.nodes.push_back(FolderNode{"{F}", "", true}); + tree.nodes.push_back(FolderNode{"{L1}", "{F}", false}); // untagged ⇒ Arrange + tree.nodes.push_back(FolderNode{"{L2}", "{F}", false}); // untagged ⇒ Arrange + + CHECK(visibleHas(vm.visibleTracks(tree, kArrangeModeId), "{F}")); + CHECK(!visibleHas(vm.visibleTracks(tree, kDesignModeId), "{F}")); + } + + // Case C: untagged folder, mixed Arrange + Design leaves ⇒ visible in both. + { + ViewModeModel vm; + FolderTree tree; + tree.nodes.push_back(FolderNode{"{F}", "", true}); + tree.nodes.push_back(FolderNode{"{LA}", "{F}", false}); // untagged ⇒ Arrange + tree.nodes.push_back(FolderNode{"{LD}", "{F}", false}); + vm.membership().tag("{LD}", kDesignModeId); + + CHECK(visibleHas(vm.visibleTracks(tree, kArrangeModeId), "{F}")); + CHECK(visibleHas(vm.visibleTracks(tree, kDesignModeId), "{F}")); + } + + // Case D: nested untagged grandparent {G} > untagged parent {F} > Design leaves. + // Both intermediate folders visible in BOTH modes: own-default Arrange (they are + // untagged), and derived Design (a Design leaf lives under each). + { + ViewModeModel vm; + FolderTree tree; + tree.nodes.push_back(FolderNode{"{G}", "", true}); + tree.nodes.push_back(FolderNode{"{F}", "{G}", true}); + tree.nodes.push_back(FolderNode{"{L1}", "{F}", false}); + tree.nodes.push_back(FolderNode{"{L2}", "{F}", false}); + vm.membership().tag("{L1}", kDesignModeId); + vm.membership().tag("{L2}", kDesignModeId); + + auto arrange = vm.visibleTracks(tree, kArrangeModeId); + auto design = vm.visibleTracks(tree, kDesignModeId); + CHECK(visibleHas(arrange, "{G}") && visibleHas(arrange, "{F}")); // own default + CHECK(visibleHas(design, "{G}") && visibleHas(design, "{F}")); // derived + // The Design leaves stay Design-only; not visible in Arrange. + CHECK(!visibleHas(arrange, "{L1}") && !visibleHas(arrange, "{L2}")); + } + + // Case E: existing behavior unchanged — a TAGGED-Design folder holding a visible + // Design leaf still shows in Design; and a folder made visible only by a visible + // tagged-Design descendant (own membership Arrange) still derives into Design. + // Leaf visibility itself is unchanged: a Design leaf is Design-only. + { + ViewModeModel vm; + FolderTree tree; + tree.nodes.push_back(FolderNode{"{F}", "", true}); + tree.nodes.push_back(FolderNode{"{LD}", "{F}", false}); + vm.membership().tag("{F}", kDesignModeId); // folder tagged into Design itself + vm.membership().tag("{LD}", kDesignModeId); + + auto design = vm.visibleTracks(tree, kDesignModeId); + CHECK(visibleHas(design, "{F}")); // own Design membership + derived from {LD} + CHECK(visibleHas(design, "{LD}")); + // A Design-tagged folder is NOT an Arrange member ⇒ not visible in Arrange + // unless a child is; here the only child is Design, so folder hidden in Arrange. + auto arrange = vm.visibleTracks(tree, kArrangeModeId); + CHECK(!visibleHas(arrange, "{F}")); + CHECK(!visibleHas(arrange, "{LD}")); // leaf visibility unchanged + } +} + // -- 3. Restore round-trip (the trust anchor) -------------------------------- static void testRestoreRoundTripSnapshotValues() { @@ -638,6 +745,7 @@ static void testNestedToggleSnapshotSurvivesRepark() { int main() { testNModeRegistryAndMembership(); testParentDerivationMultiMode(); + testParentOwnMembershipVisibility(); testRestoreRoundTripSnapshotValues(); testShowBothNeverParkedVisibleEverywhere(); testStaleGuidTolerated(); From 2c9b635edf3890b68e15216edcc3ae317e13800e Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Thu, 23 Jul 2026 06:22:16 -0400 Subject: [PATCH 2/2] fix(view): update parent-visibility comment to describe OR rule (own membership or descendant visible) --- src/view.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/view.cpp b/src/view.cpp index 2c6af50..bb7d72b 100644 --- a/src/view.cpp +++ b/src/view.cpp @@ -184,11 +184,11 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject model.clearSnapshot(guid); } - // PARENT VISIBILITY (derived, never parked): a folder is visible iff at least - // one of its descendant leaves is visible in the target mode. That derivation is - // pure (membership + active mode), so it is recomputed every toggle rather than - // snapshotted — the four leaf-park flags don't apply to parents. Drive only the - // two visibility flags; never touch B_MAINSEND/I_FXEN/FX-offline on a parent. + // 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;