Defer Design View's per-FX park to an idle tick; honest undo mask, compare-before-write, PreventUIRefresh bracket, O(1) handle resolve
This commit is contained in:
+142
-159
@@ -10,6 +10,7 @@
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -18,6 +19,7 @@
|
||||
#include "core/view/lane_keys.h"
|
||||
#include "core/view/solo_cache.h"
|
||||
#include "shell/capture/track_guid.h"
|
||||
#include "shell/view/view_fx_park.h"
|
||||
#include "shell/view/view_solo.h"
|
||||
#include "core/view/view_tree.h"
|
||||
|
||||
@@ -28,12 +30,8 @@
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
|
||||
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||
#define REAPERAPI_WANT_TrackFX_GetCount
|
||||
#define REAPERAPI_WANT_TrackFX_GetFXGUID
|
||||
#define REAPERAPI_WANT_PreventUIRefresh
|
||||
#define REAPERAPI_WANT_TrackFX_GetOffline
|
||||
#define REAPERAPI_WANT_TrackFX_SetOffline
|
||||
#define REAPERAPI_WANT_guidToString
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
||||
@@ -64,6 +62,26 @@ constexpr int kFreeModeFixedLanes = 2;
|
||||
// deliberately excluded, nothing is moving there.
|
||||
constexpr int kTransportMoving = 1 | 4;
|
||||
|
||||
// The domains one apply actually writes: track config/routing (visibility,
|
||||
// B_MAINSEND, I_SOLO), item lane assignment, and the ext-state block
|
||||
// (UNDO_STATE_MISCCFG covers extension state). NOT UNDO_STATE_ALL, which
|
||||
// includes UNDO_STATE_FX and so makes every undo record carry every track's FX
|
||||
// state — a cost that scales with the project's plugin count rather than with
|
||||
// what the switch changed. FX is OR'd back in per apply, only when an FX-domain
|
||||
// flag really moved.
|
||||
constexpr int kApplyUndoMask = UNDO_STATE_TRACKCFG | UNDO_STATE_ITEMS | UNDO_STATE_MISCCFG;
|
||||
|
||||
// Holds REAPER's UI refresh off for the write phase; the deliberate rebuild
|
||||
// (TrackList_AdjustWindows + UpdateArrange) runs after it releases. RAII because
|
||||
// an unbalanced pair leaves the user's UI frozen with no way back — the SDK's
|
||||
// own warning at reaper_plugin_functions.h:5581.
|
||||
struct UiRefreshHold {
|
||||
UiRefreshHold() { PreventUIRefresh(1); }
|
||||
~UiRefreshHold() { PreventUIRefresh(-1); }
|
||||
UiRefreshHold(const UiRefreshHold&) = delete;
|
||||
UiRefreshHold& operator=(const UiRefreshHold&) = delete;
|
||||
};
|
||||
|
||||
// C_LANESCOLLAPSED=2: render a tool-split track like a normal single-lane
|
||||
// track showing only the playing lane (SDK: 1=collapsed, 2=hidden-lanes-exist
|
||||
// but displays as non-fixed-lane).
|
||||
@@ -102,7 +120,7 @@ const char* flagParm(Flag f) {
|
||||
// construction. Also caches each MediaTrack* by GUID for later resolve().
|
||||
std::vector<TrackFolderEntry> readFolderEntries(
|
||||
ReaProject* proj,
|
||||
std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
||||
TrackHandles& handleByGuid) {
|
||||
std::vector<TrackFolderEntry> entries;
|
||||
int count = CountTracks(proj);
|
||||
entries.reserve(static_cast<std::size_t>(count));
|
||||
@@ -119,46 +137,21 @@ std::vector<TrackFolderEntry> readFolderEntries(
|
||||
return entries;
|
||||
}
|
||||
|
||||
MediaTrack* resolve(const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid,
|
||||
const std::string& guid) {
|
||||
for (const auto& kv : handleByGuid) {
|
||||
if (kv.first == guid) return kv.second;
|
||||
}
|
||||
return nullptr; // stale/deleted GUID — pruned by being skipped
|
||||
// Every park, restore, parent and lane op resolves its GUID; a linear scan made
|
||||
// that O(T²) on the one path whose cost the user waits on. The vector form
|
||||
// survives beside it because view_solo's writers take one pass over it.
|
||||
using TrackByGuid = std::unordered_map<std::string, MediaTrack*>;
|
||||
|
||||
TrackByGuid indexHandles(const TrackHandles& handleByGuid) {
|
||||
TrackByGuid byGuid;
|
||||
byGuid.reserve(handleByGuid.size());
|
||||
for (const auto& kv : handleByGuid) byGuid.emplace(kv.first, kv.second);
|
||||
return byGuid;
|
||||
}
|
||||
|
||||
// The FX's own durable identity, braced exactly like the track GUID keys. Empty
|
||||
// when REAPER reports none — an FX we cannot name is one we cannot restore, and
|
||||
// fx_offline treats it that way rather than guessing at its slot. Lifetime is
|
||||
// settled: the string copy is taken immediately and the GUID* is never held
|
||||
// past this call (reaper_plugin_functions.h:7348 documents no null contract for
|
||||
// TrackFX_GetFXGUID; treating null as "no identity" is the safe read).
|
||||
//
|
||||
// [verify — DAW] STABILITY across a chain mutation is not settled the same way:
|
||||
// confirm the GUID for one FX instance survives a native drag-reorder, an SWS
|
||||
// move (SNM_MoveOrRemoveTrackFX — SWS issue #802 reports the FXID lines do not
|
||||
// follow the plugin after that call, i.e. wrong-plugin restores or mass drops
|
||||
// through fx_offline on that path specifically), a save/reload round trip, and
|
||||
// two live instances of one plugin type staying distinguishable. See
|
||||
// src/shell/view/CLAUDE.md's Gotchas for the SWS-path risk this leaves open.
|
||||
std::string fxGuidString(MediaTrack* tr, int fx) {
|
||||
GUID* g = TrackFX_GetFXGUID(tr, fx);
|
||||
if (!g) return {};
|
||||
char buf[64] = {0}; // guidToString needs a >=64-char destination (SDK contract)
|
||||
guidToString(g, buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// The chain as it stands now: identity by current slot. Snapshot, park and
|
||||
// restore all address FX through this one plain 0..TrackFX_GetCount-1
|
||||
// enumeration — never the 0x1000000/0x2000000 input-FX or container forms — so
|
||||
// whatever it covers, all three cover identically.
|
||||
std::vector<std::string> liveFxGuids(MediaTrack* tr) {
|
||||
const int fxCount = TrackFX_GetCount(tr);
|
||||
std::vector<std::string> guids;
|
||||
guids.reserve(static_cast<std::size_t>(fxCount));
|
||||
for (int fx = 0; fx < fxCount; ++fx) guids.push_back(fxGuidString(tr, fx));
|
||||
return guids;
|
||||
MediaTrack* resolve(const TrackByGuid& byGuid, const std::string& guid) {
|
||||
auto it = byGuid.find(guid);
|
||||
return it == byGuid.end() ? nullptr : it->second; // stale/deleted GUID — pruned by being skipped
|
||||
}
|
||||
|
||||
// Captures prior driven-flag state before parking. Never reads B_MUTE/I_SOLO;
|
||||
@@ -179,30 +172,25 @@ TrackSnapshot snapshotTrack(MediaTrack* tr) {
|
||||
return snap; // fxKeying stays Identity — a live capture always knows the chain
|
||||
}
|
||||
|
||||
void applyFlags(MediaTrack* tr, const std::vector<TrackFlagOp>& flags) {
|
||||
// A reapply (tag/untag, project load) re-plans every flag it already applied, so
|
||||
// most of what it writes is a value the track already holds; the read is the
|
||||
// cheap half of the pair, and it also keeps those no-op writes out of the undo
|
||||
// record's mask.
|
||||
bool writeIfChanged(MediaTrack* tr, const char* parm, double value) {
|
||||
if (GetMediaTrackInfo_Value(tr, parm) == value) return false;
|
||||
SetMediaTrackInfo_Value(tr, parm, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
void applyFlags(MediaTrack* tr, const std::vector<TrackFlagOp>& flags, int& undoMask) {
|
||||
for (const TrackFlagOp& op : flags) {
|
||||
SetMediaTrackInfo_Value(tr, flagParm(op.flag), static_cast<double>(op.value));
|
||||
if (!writeIfChanged(tr, flagParm(op.flag), static_cast<double>(op.value))) continue;
|
||||
// I_FXEN is the only FX-domain flag driven here, so it is the only one
|
||||
// whose undo record has to carry UNDO_STATE_FX.
|
||||
if (op.flag == Flag::FxEnable) undoMask |= UNDO_STATE_FX;
|
||||
}
|
||||
}
|
||||
|
||||
// The pure park plan leaves fxOffline empty by design; expand it here from the
|
||||
// live FX count.
|
||||
void parkFxOffline(MediaTrack* tr) {
|
||||
int fxCount = TrackFX_GetCount(tr);
|
||||
for (int fx = 0; fx < fxCount; ++fx) {
|
||||
TrackFX_SetOffline(tr, fx, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Restores per-FX offline from the snapshot verbatim — never a blanket "online".
|
||||
// Which live FX each captured state belongs to is resolveFxRestore's call, and
|
||||
// what it could not place comes back for the caller to report.
|
||||
FxRestoreDrops restoreFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& fxOffline) {
|
||||
const FxRestoreResolution res = resolveFxRestore(fxOffline, liveFxGuids(tr));
|
||||
for (const FxOfflineWrite& w : res.writes) TrackFX_SetOffline(tr, w.fxIndex, w.offline);
|
||||
return res.drops;
|
||||
}
|
||||
|
||||
// Managed-lane application: the pure planner keys LanePlayOps by the lane's
|
||||
// DURABLE name; REAPER's C_LANEPLAYS:N is keyed by current ordinal, which
|
||||
// renumbers on reorder. So every write here re-resolves durable key -> current
|
||||
@@ -238,7 +226,7 @@ std::map<std::string, int> managedLaneOrdinals(MediaTrack* tr) {
|
||||
void applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) {
|
||||
char parm[32];
|
||||
std::snprintf(parm, sizeof(parm), "C_LANEPLAYS:%d", laneIdx);
|
||||
SetMediaTrackInfo_Value(tr, parm, static_cast<double>(lanePlays));
|
||||
writeIfChanged(tr, parm, static_cast<double>(lanePlays));
|
||||
}
|
||||
|
||||
// Groups ops by track, reconciles each op's durable laneKey to the track's
|
||||
@@ -246,7 +234,7 @@ void applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) {
|
||||
// enables fixed-lane mode on any track carrying a managed lane, and drives
|
||||
// C_LANEPLAYS. UpdateTimeline() is the caller's job when this returns true
|
||||
// (SDK: required after an I_FREEMODE change).
|
||||
bool applyLaneOps(const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid,
|
||||
bool applyLaneOps(const TrackByGuid& handleByGuid,
|
||||
const std::vector<LanePlayOp>& lanes) {
|
||||
if (lanes.empty()) return false;
|
||||
|
||||
@@ -312,7 +300,7 @@ std::string itemModeFromMembership(const ViewModeModel& model, const std::string
|
||||
// there regardless of name).
|
||||
std::vector<LaneTrack> readLaneTracks(
|
||||
const ViewModeModel& model,
|
||||
const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
||||
const TrackHandles& handleByGuid) {
|
||||
std::vector<LaneTrack> tracks;
|
||||
tracks.reserve(handleByGuid.size());
|
||||
for (const auto& [guid, tr] : handleByGuid) {
|
||||
@@ -358,7 +346,7 @@ bool assignItemToLane(MediaTrack* tr, MediaItem* it, int laneOrdinal) {
|
||||
// managed-eligible items; I_NUMFIXEDLANES is only ever GROWN, never shrunk,
|
||||
// so a user's existing manual lanes are never renamed or reassigned.
|
||||
bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
||||
const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid) {
|
||||
const TrackByGuid& handleByGuid) {
|
||||
bool changed = false;
|
||||
|
||||
std::map<std::string, std::vector<const LaneMint*>> mintsByTrack;
|
||||
@@ -436,8 +424,9 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
return false; // same fail-closed shape as the mode-exists guard above
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
||||
TrackHandles handleByGuid;
|
||||
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
|
||||
const TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
||||
FolderTree tree = buildFolderTree(entries);
|
||||
|
||||
// Prune snapshots for tracks no longer in the live enumeration before
|
||||
@@ -455,81 +444,6 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
|
||||
TogglePlan plan = model.planToggle(tree, targetModeId);
|
||||
|
||||
Undo_BeginBlock2(proj);
|
||||
|
||||
// DISJOIN THE SOLO SURFACES. Both this clear and the replay after setActiveMode
|
||||
// ride the existing undo block — one mode toggle stays one Ctrl-Z.
|
||||
if (realSwitch) {
|
||||
model.soloCache().store(outgoingModeId, outgoingSolo);
|
||||
clearTrackSolos(handleByGuid, outgoingSolo);
|
||||
}
|
||||
|
||||
// PARK: snapshot before mutating, store into the model, then apply.
|
||||
for (const TrackPlan& tp : plan.park) {
|
||||
if (tp.flags.empty()) continue; // every op in a TrackPlan targets one track
|
||||
const std::string& guid = tp.flags.front().guid;
|
||||
MediaTrack* tr = resolve(handleByGuid, guid);
|
||||
if (!tr) continue; // stale GUID — prune
|
||||
|
||||
// Snapshot ONCE, at first park: a snapshot already present means the
|
||||
// track is still parked from a prior apply, so its live flags are the
|
||||
// parked values — recapturing would overwrite the true pre-park state
|
||||
// with zeros and a later restore would hide it for good. Restore
|
||||
// clears the snapshot, so the next genuine park recaptures fresh state.
|
||||
if (model.snapshot(guid) == nullptr)
|
||||
model.storeSnapshot(guid, snapshotTrack(tr));
|
||||
applyFlags(tr, tp.flags);
|
||||
parkFxOffline(tr);
|
||||
}
|
||||
|
||||
// RESTORE: apply verbatim, then drop the consumed snapshot.
|
||||
FxRestoreDrops fxDrops;
|
||||
int fxDropTracks = 0;
|
||||
for (const TrackPlan& tp : plan.restore) {
|
||||
if (tp.flags.empty()) continue;
|
||||
const std::string& guid = tp.flags.front().guid;
|
||||
MediaTrack* tr = resolve(handleByGuid, guid);
|
||||
if (!tr) continue; // stale GUID — prune
|
||||
|
||||
applyFlags(tr, tp.flags);
|
||||
const FxRestoreDrops drops = restoreFxOffline(tr, tp.fxOffline);
|
||||
if (drops.total() > 0) {
|
||||
fxDrops.add(drops);
|
||||
++fxDropTracks;
|
||||
}
|
||||
model.clearSnapshot(guid);
|
||||
}
|
||||
|
||||
// Captured FX state that could not be applied is REPORTED. Silence here would
|
||||
// read to the user as "restore worked" while an FX sat at whatever state the
|
||||
// park left it in. Sent with the "!SHOW:" prefix (reaper_plugin_functions.h:6536)
|
||||
// so it never force-opens the console window: applyMode's reapply path also
|
||||
// runs unattended on project load (see the reconcile comment above), and this
|
||||
// one call site can't tell that case apart from an interactive toggle/tag-edit
|
||||
// reapply — both call in with target == active — so splitting loud-on-toggle
|
||||
// from quiet-on-load would need a flag threaded from every caller, several of
|
||||
// which are outside this change. Quiet-always is the safe default: the message
|
||||
// still lands in the console for whoever opens it, on every path.
|
||||
const std::string fxDropMsg = describeFxRestoreDrops(fxDrops, fxDropTracks);
|
||||
if (!fxDropMsg.empty()) ShowConsoleMsg(("!SHOW:" + fxDropMsg).c_str());
|
||||
|
||||
// MANAGED LANES: drive C_LANEPLAYS so the active mode's lane plays+shows
|
||||
// and every other managed lane is silenced+hidden. Empty for a D1-only
|
||||
// project, leaving that behavior byte-identical.
|
||||
const bool laneModeChanged = applyLaneOps(handleByGuid, plan.lanes);
|
||||
|
||||
// PARENT VISIBILITY (never parked): recomputed every toggle, never
|
||||
// snapshotted. Only the two visibility flags — never mainSend/FX on a parent.
|
||||
std::set<std::string> visible = model.visibleTracks(tree, targetModeId);
|
||||
for (const FolderNode& node : tree.nodes) {
|
||||
if (!node.isParent) continue;
|
||||
MediaTrack* tr = resolve(handleByGuid, node.guid);
|
||||
if (!tr) continue; // stale GUID — prune
|
||||
double show = visible.count(node.guid) ? 1.0 : 0.0;
|
||||
SetMediaTrackInfo_Value(tr, "B_SHOWINTCP", show);
|
||||
SetMediaTrackInfo_Value(tr, "B_SHOWINMIXER", show);
|
||||
}
|
||||
|
||||
// Target is guaranteed registered (checked at entry); fall back to the id
|
||||
// defensively if that ever changes.
|
||||
const Mode* targetMode = model.modes().query(targetModeId);
|
||||
@@ -537,16 +451,81 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
"ReaSampler: activate " +
|
||||
(targetMode ? targetMode->displayName : targetModeId) + " view";
|
||||
|
||||
model.setActiveMode(targetModeId);
|
||||
int undoMask = kApplyUndoMask;
|
||||
bool laneModeChanged = false;
|
||||
|
||||
// Replay + consume, before the single relayout below picks the change up. Dropped
|
||||
// against `visible` (computed above for parent visibility), not the park plan: a
|
||||
// folder parent can be hidden without being parked, and a hidden track must not
|
||||
// receive a replayed solo it carries no visible control to undo.
|
||||
if (realSwitch) {
|
||||
if (const std::map<std::string, int>* cached = model.soloCache().query(targetModeId)) {
|
||||
restoreTrackSolos(handleByGuid, *cached, liveGuids, visible);
|
||||
model.soloCache().clear(targetModeId);
|
||||
Undo_BeginBlock2(proj);
|
||||
{
|
||||
UiRefreshHold uiHold; // every write below lands with the TCP/MCP frozen
|
||||
|
||||
// DISJOIN THE SOLO SURFACES. Both this clear and the replay after
|
||||
// setActiveMode ride the existing undo block — one mode toggle stays one Ctrl-Z.
|
||||
if (realSwitch) {
|
||||
model.soloCache().store(outgoingModeId, outgoingSolo);
|
||||
clearTrackSolos(handleByGuid, outgoingSolo);
|
||||
}
|
||||
|
||||
// PARK: snapshot before mutating, store into the model, then apply. The
|
||||
// per-FX offline half is deferred (view_fx_park) — it is the expensive
|
||||
// half and nothing about the new mode's appearance waits on it.
|
||||
for (const TrackPlan& tp : plan.park) {
|
||||
if (tp.flags.empty()) continue; // every op in a TrackPlan targets one track
|
||||
const std::string& guid = tp.flags.front().guid;
|
||||
MediaTrack* tr = resolve(trackByGuid, guid);
|
||||
if (!tr) continue; // stale GUID — prune
|
||||
|
||||
// Snapshot ONCE, at first park: a snapshot already present means the
|
||||
// track is still parked from a prior apply (or its deferred restore
|
||||
// has not landed yet), so its live flags are the parked values —
|
||||
// recapturing would overwrite the true pre-park state with zeros and
|
||||
// a later restore would hide it for good.
|
||||
if (model.snapshot(guid) == nullptr)
|
||||
model.storeSnapshot(guid, snapshotTrack(tr));
|
||||
applyFlags(tr, tp.flags, undoMask);
|
||||
deferFxPark(proj, guid);
|
||||
}
|
||||
|
||||
// RESTORE: flags verbatim now, per-FX offline on the drain, which is also
|
||||
// where the consumed snapshot is dropped.
|
||||
for (const TrackPlan& tp : plan.restore) {
|
||||
if (tp.flags.empty()) continue;
|
||||
const std::string& guid = tp.flags.front().guid;
|
||||
MediaTrack* tr = resolve(trackByGuid, guid);
|
||||
if (!tr) continue; // stale GUID — prune
|
||||
|
||||
applyFlags(tr, tp.flags, undoMask);
|
||||
deferFxRestore(proj, guid, tp.fxOffline);
|
||||
}
|
||||
|
||||
// MANAGED LANES: drive C_LANEPLAYS so the active mode's lane plays+shows
|
||||
// and every other managed lane is silenced+hidden. Empty for a D1-only
|
||||
// project, leaving that behavior byte-identical.
|
||||
laneModeChanged = applyLaneOps(trackByGuid, plan.lanes);
|
||||
|
||||
// PARENT VISIBILITY (never parked): recomputed every toggle, never
|
||||
// snapshotted. Only the two visibility flags — never mainSend/FX on a parent.
|
||||
std::set<std::string> visible = model.visibleTracks(tree, targetModeId);
|
||||
for (const FolderNode& node : tree.nodes) {
|
||||
if (!node.isParent) continue;
|
||||
MediaTrack* tr = resolve(trackByGuid, node.guid);
|
||||
if (!tr) continue; // stale GUID — prune
|
||||
const double show = visible.count(node.guid) ? 1.0 : 0.0;
|
||||
writeIfChanged(tr, "B_SHOWINTCP", show);
|
||||
writeIfChanged(tr, "B_SHOWINMIXER", show);
|
||||
}
|
||||
|
||||
model.setActiveMode(targetModeId);
|
||||
|
||||
// Replay + consume, before the single relayout below picks the change up.
|
||||
// Dropped against `visible` (computed above for parent visibility), not the
|
||||
// park plan: a folder parent can be hidden without being parked, and a
|
||||
// hidden track must not receive a replayed solo it carries no visible
|
||||
// control to undo.
|
||||
if (realSwitch) {
|
||||
if (const std::map<std::string, int>* cached = model.soloCache().query(targetModeId)) {
|
||||
restoreTrackSolos(handleByGuid, *cached, liveGuids, visible);
|
||||
model.soloCache().clear(targetModeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,12 +539,12 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
// fixed lanes this apply (SDK requirement for I_FREEMODE changes).
|
||||
if (laneModeChanged) UpdateTimeline();
|
||||
|
||||
Undo_EndBlock2(proj, undoLabel.c_str(), -1);
|
||||
Undo_EndBlock2(proj, undoLabel.c_str(), undoMask);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
||||
TrackHandles handleByGuid;
|
||||
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
|
||||
// The tree is needed to detect a content-bearing folder derived-visible in
|
||||
// >1 mode, exactly as applyMode builds it.
|
||||
@@ -575,8 +554,10 @@ bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||
const LaneMintPlan plan = planLaneMinting(model, tree, tracks);
|
||||
if (plan.empty()) return false; // nothing to mint — no Undo point for a no-op tick
|
||||
|
||||
const TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
||||
|
||||
Undo_BeginBlock2(proj);
|
||||
const bool changed = applyMintPlan(model, plan, handleByGuid);
|
||||
const bool changed = applyMintPlan(model, plan, trackByGuid);
|
||||
|
||||
if (!changed) {
|
||||
// Plan was non-empty but every write was already satisfied — discard
|
||||
@@ -599,17 +580,19 @@ bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||
// here — it would re-park/restore whole tracks and recompute parent
|
||||
// visibility, which a lane-only mint must not touch.
|
||||
const TogglePlan togglePlan = model.planToggle(FolderTree{}, model.activeModeId());
|
||||
applyLaneOps(handleByGuid, togglePlan.lanes);
|
||||
applyLaneOps(trackByGuid, togglePlan.lanes);
|
||||
|
||||
UpdateTimeline(); // a split happened this call — refresh is owed
|
||||
UpdateArrange();
|
||||
|
||||
Undo_EndBlock2(proj, "ReaSampler: separate cross-mode content into lanes", -1);
|
||||
// Lane minting writes track lane config and item lane assignment and nothing
|
||||
// else — never an FX state — so it takes the same honest mask applyMode does.
|
||||
Undo_EndBlock2(proj, "ReaSampler: separate cross-mode content into lanes", kApplyUndoMask);
|
||||
return true;
|
||||
}
|
||||
|
||||
void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
||||
TrackHandles handleByGuid;
|
||||
readFolderEntries(proj, handleByGuid); // populates handleByGuid (tree unused here)
|
||||
|
||||
// Pure read of REAPER state (no lane created, no I_FREEMODE/I_NUMFIXEDLANES/
|
||||
|
||||
Reference in New Issue
Block a user