Merge fix: parent visible by own membership or descendant (untagged folders show in both modes)

This commit is contained in:
2026-07-23 06:27:24 -04:00
4 changed files with 139 additions and 20 deletions
+5 -5
View File
@@ -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<std::string> visible = model.visibleTracks(tree, targetModeId);
for (const FolderNode& node : tree.nodes) {
if (!node.isParent) continue;
+16 -9
View File
@@ -172,24 +172,31 @@ std::set<std::string> ViewModeModel::visibleTracks(const FolderTree& tree,
const std::string& modeId) const {
std::set<std::string> 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<std::string, std::string> 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<std::string> 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;
+8 -4
View File
@@ -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<std::string> visibleTracks(const FolderTree& tree,
const std::string& modeId) const;
+110 -2
View File
@@ -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();