fix(design-view): adopt pre-existing track mode on drop to prevent lane strand

A bank-panel capture dropped onto a track with pre-existing content was
auto-tagged into the active mode, forcing a multi-mode lane split whose
toggle silenced the pre-existing items. New items now adopt the single
mode of their track's prior content; deliberate splits stay on the
explicit item-move path.
This commit is contained in:
2026-07-27 04:50:46 -04:00
parent e0d4358452
commit 8887995d7a
4 changed files with 238 additions and 10 deletions
+53 -4
View File
@@ -1750,12 +1750,16 @@ bool isFixedLaneTrack(MediaTrack* tr) {
// Enumerates the live project's track + item GUIDs. Fills `allGuids` (the full live set,
// baseline input) and, for each item, records whether it sits on a manual lane so a
// newly-detected item can be exempted from auto-tag without a second project walk.
// `trackItemGuids` additionally maps each track GUID to the item GUIDs it carries, so a
// newly-detected item's PRE-EXISTING siblings can be resolved (the adoption / strand
// guard) without a second project walk.
//
// Manual-lane classification uses the single pure predicate isOnManualLane(isFixedLaneTrack,
// laneName) from lane_keys — the same predicate the apply path consults — so the exemption
// rule is defined in exactly one place and is unit-tested there.
void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
std::map<std::string, bool>& itemOnManualLane) {
std::map<std::string, bool>& itemOnManualLane,
std::map<std::string, std::vector<std::string>>& trackItemGuids) {
const int trackCount = CountTracks(proj);
for (int t = 0; t < trackCount; ++t) {
MediaTrack* tr = GetTrack(proj, t);
@@ -1767,6 +1771,7 @@ void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
// track-level attribute and is the same for every item on the track.
const bool fixedLane = isFixedLaneTrack(tr);
std::vector<std::string>& itemsOnTrack = trackItemGuids[tg];
const int itemCount = CountTrackMediaItems(tr);
for (int i = 0; i < itemCount; ++i) {
MediaItem* it = GetTrackMediaItem(tr, i);
@@ -1779,6 +1784,7 @@ void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
// false immediately for non-fixed-lane tracks regardless of name).
const std::string ln = fixedLane ? itemLaneName(tr, it) : std::string{};
itemOnManualLane[ig] = isOnManualLane(fixedLane, ln);
itemsOnTrack.push_back(ig);
}
}
}
@@ -1818,11 +1824,52 @@ bool detectNewContent() {
std::set<std::string> live;
std::map<std::string, bool> itemOnManualLane;
enumerateLiveGuids(proj, live, itemOnManualLane);
std::map<std::string, std::vector<std::string>> trackItemGuids;
enumerateLiveGuids(proj, live, itemOnManualLane, trackItemGuids);
const std::vector<std::string> added = g_panel.contentBaseline.observe(live);
if (added.empty()) return false; // first poll after open, or nothing new this tick
ViewModeModel& model = g_panel.session->view();
// Which of `added` are items (the manual-lane map keys every item; track GUIDs never
// appear there). Used below to exclude sibling new items from a track's PRE-EXISTING
// mode set — a drop plus its own new siblings must not count each other as prior.
const std::set<std::string> newItemGuids = [&] {
std::set<std::string> s;
for (const std::string& g : added)
if (itemOnManualLane.count(g)) s.insert(g);
return s;
}();
// Item guid -> its track guid (reverse of trackItemGuids), so a new item's siblings
// are found in one lookup.
std::map<std::string, std::string> trackOfItem;
for (const auto& [trackGuid, items] : trackItemGuids)
for (const std::string& ig : items) trackOfItem[ig] = trackGuid;
// The distinct modes the PRE-EXISTING (not-new-this-tick) MANAGED-ELIGIBLE items on
// `trackGuid` resolve to. Untagged siblings resolve to Arrange (leafBelongsToMode's
// default); new siblings are excluded; manual-lane siblings are EXEMPT — exactly as
// planLaneMinting ignores them when computing a track's own-item mode span, so the
// adoption guard's view of the track matches the split decision's. Drives the adoption
// / strand guard in autoTagNewContent.
const auto preExistingTrackModes =
[&](const std::string& trackGuid) -> std::set<std::string> {
std::set<std::string> modes;
auto it = trackItemGuids.find(trackGuid);
if (it == trackItemGuids.end()) return modes;
for (const std::string& sib : it->second) {
if (newItemGuids.count(sib)) continue; // a sibling added THIS tick — not prior
auto ml = itemOnManualLane.find(sib);
if (ml != itemOnManualLane.end() && ml->second) continue; // manual lane — exempt
const std::set<std::string> m = model.membership().modesOf(sib);
if (m.empty()) modes.insert(kArrangeModeId); // untagged ⇒ Arrange default
else modes.insert(m.begin(), m.end());
}
return modes;
};
// Split the new GUIDs into tracks vs items so the pure decision can apply the
// manual-lane exemption to items only. A GUID present in the item-lane map is an
// item; otherwise it is a track (track GUIDs never appear in that map).
@@ -1833,11 +1880,13 @@ bool detectNewContent() {
if (it == itemOnManualLane.end()) {
newTracks.push_back(g); // a track GUID
} else {
newItems.push_back(NewItem{g, it->second}); // an item; carries its exemption
NewItem ni{g, it->second, {}};
auto tk = trackOfItem.find(g);
if (tk != trackOfItem.end()) ni.trackModes = preExistingTrackModes(tk->second);
newItems.push_back(std::move(ni)); // an item; carries exemption + track modes
}
}
ViewModeModel& model = g_panel.session->view();
const std::vector<AutoTag> tags =
autoTagNewContent(newTracks, newItems, model.activeModeId());
for (const AutoTag& tag : tags)