Reconcile orphaned Design View snapshots on track delete

Add ViewModeModel::reconcile(liveGuids) to prune snapshots of deleted
tracks; wire it into applyMode before planning. Keep membership so
undo-delete (same GUID) preserves the Design tag.
This commit is contained in:
2026-07-23 13:27:42 -04:00
parent dcbe5d7bdc
commit f9af111c7a
4 changed files with 141 additions and 0 deletions
+10
View File
@@ -144,6 +144,16 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
FolderTree tree = buildFolderTree(entries);
// Reconcile orphaned model state BEFORE planning: prune snapshots whose track was
// deleted from the project (its GUID no longer appears in the live enumeration).
// handleByGuid holds every currently-enumerated track GUID, so its keys are the
// authoritative live set. Membership is intentionally NOT pruned (undo-delete
// restores the same GUID — see ViewModeModel::reconcile). Because reapply-on-load
// routes through applyMode, this also reconciles on project open.
std::set<std::string> liveGuids;
for (const auto& kv : handleByGuid) liveGuids.insert(kv.first);
model.reconcile(liveGuids);
TogglePlan plan = model.planToggle(tree, targetModeId);
Undo_BeginBlock2(proj);
+15
View File
@@ -160,6 +160,21 @@ const TrackSnapshot* ViewModeModel::snapshot(const std::string& guid) const {
return it == snapshots_.end() ? nullptr : &it->second;
}
std::size_t ViewModeModel::reconcile(const std::set<std::string>& liveGuids) {
// Prune snapshots for GUIDs the project no longer contains (see header for the
// deliberate snapshot-yes / membership-no asymmetry and the undo-delete rationale).
std::size_t removed = 0;
for (auto it = snapshots_.begin(); it != snapshots_.end();) {
if (liveGuids.count(it->first) == 0) {
it = snapshots_.erase(it);
++removed;
} else {
++it;
}
}
return removed;
}
bool ViewModeModel::leafBelongsToMode(const std::string& guid, const std::string& modeId) const {
const Membership* m = membership_.query(guid);
if (!m) return modeId == kArrangeModeId; // untagged ⇒ Arrange default
+20
View File
@@ -267,6 +267,26 @@ public:
const TrackSnapshot* snapshot(const std::string& guid) const;
const std::map<std::string, TrackSnapshot>& snapshots() const { return snapshots_; }
// Prunes orphaned per-track state: drops every snapshot whose GUID is NOT in
// `liveGuids` (the set of GUIDs the shell currently enumerates from the project).
// Returns the number of snapshots removed. The shell calls this before planning a
// toggle; because reapply-on-load also routes through the shell's applyMode, this
// reconciles on project open too.
//
// Why snapshots and NOT membership: a parked track's snapshot is dead weight once
// the track is deleted — it can never be restored, and if REAPER reuses that GUID
// for a different track a stale snapshot would drive an INCORRECT restore. So it
// must be pruned. Membership is deliberately KEPT: REAPER's undo of a track delete
// restores the SAME GUID, so dropping the Design tag on delete would silently lose
// it on undo-delete. Keeping membership means an undone delete brings the track
// back correctly tagged and it re-snapshots + re-parks cleanly on the next toggle.
// A genuinely-deleted-and-never-restored track leaves only a tiny dormant
// membership entry — acceptable, and far better than losing tags on undo. Folder
// RESTRUCTURE (moving tracks without deleting) is already self-healing: the tree is
// rebuilt from I_FOLDERDEPTH every toggle, so a restructure leaves every GUID live
// and reconcile is a no-op over it. This handles DELETION specifically.
std::size_t reconcile(const std::set<std::string>& liveGuids);
// Does `guid` belong to `modeId`? A leaf belongs if it is tagged into modeId,
// is show-both (belongs everywhere), or is untagged and modeId is Arrange (the
// default). Parent derivation is NOT applied here — this is the LEAF rule; use