ab14ae769f
Extends the OnTimer guard to skip session.poll() itself while modeApplyInProgress(), preventing an undo/redo or project-switch reload from replacing the view model under an in-flight applyMode.
626 lines
29 KiB
C++
626 lines
29 KiB
C++
// See view.h. Compiled into the reaper_reasampler module; includes
|
|
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
|
// Tree arithmetic lives in view_tree (pure) and one track's park/restore in
|
|
// view_fx_park; this file owns the plan iteration, lanes, parents and the one
|
|
// undo block they all ride.
|
|
|
|
#include "shell/view/view.h"
|
|
|
|
#include <cstdio>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "shell/capture/item_read.h"
|
|
#include "core/view/fx_offline.h"
|
|
#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"
|
|
|
|
#define REAPERAPI_MINIMAL
|
|
#define REAPERAPI_WANT_CountTracks
|
|
#define REAPERAPI_WANT_EnumProjects
|
|
#define REAPERAPI_WANT_GetPlayStateEx
|
|
#define REAPERAPI_WANT_GetTrack
|
|
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
|
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
|
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
|
|
#define REAPERAPI_WANT_PreventUIRefresh
|
|
#define REAPERAPI_WANT_Undo_BeginBlock2
|
|
#define REAPERAPI_WANT_Undo_EndBlock2
|
|
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
|
#define REAPERAPI_WANT_UpdateArrange
|
|
#define REAPERAPI_WANT_UpdateTimeline
|
|
#define REAPERAPI_WANT_ValidatePtr2
|
|
// Lane minting: item-side lane reads/writes assigning each item to its mode's lane.
|
|
#define REAPERAPI_WANT_CountTrackMediaItems
|
|
#define REAPERAPI_WANT_GetTrackMediaItem
|
|
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
|
#define REAPERAPI_WANT_SetMediaItemInfo_Value
|
|
#include "reaper_plugin_functions.h"
|
|
|
|
namespace reasampler {
|
|
|
|
using view::buildFolderTree;
|
|
using view::isOnManualLane;
|
|
using view::managedLaneKey;
|
|
using view::modeIdFromLaneName;
|
|
using view::TrackFolderEntry;
|
|
|
|
namespace {
|
|
|
|
// I_FREEMODE value for fixed lanes. SDK: 0=normal, 1=free item positioning, 2=fixed lanes.
|
|
constexpr int kFreeModeFixedLanes = 2;
|
|
|
|
// GetPlayStateEx bitmask. SDK: &1 playing, &2 paused, &4 recording — paused is
|
|
// 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 I_FXEN or a
|
|
// per-FX offline state really moved (TrackApplyResult::fxMoved).
|
|
// [verify — DAW] INFERRED, not documented: UNDO_STATE_TRACKCFG reads
|
|
// "track/master vol/pan/routing" (reaper_plugin.h:1541) and names neither
|
|
// B_SHOWINTCP/B_SHOWINMIXER nor the fixed-lane properties (assumed to ride
|
|
// UNDO_STATE_ITEMS, :1543). Wrong ⇒ a Ctrl-Z restores less than the switch did.
|
|
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;
|
|
};
|
|
|
|
// applyMode is reachable from the load tick, the actions and render_in_place, and its
|
|
// TrackFX_SetOffline calls are assumed to pump the message loop — so any of them can
|
|
// re-enter it mid-apply, planning against the same model the outer loop is iterating.
|
|
// An inner restoreTrack consuming a snapshot the outer sits mid-park over leaves that
|
|
// track flags-restored, chain-offline and snapshotless: refused forever. Fail closed.
|
|
bool g_applying = false;
|
|
struct ApplyLatch {
|
|
ApplyLatch() { g_applying = true; }
|
|
~ApplyLatch() { g_applying = false; }
|
|
ApplyLatch(const ApplyLatch&) = delete;
|
|
ApplyLatch& operator=(const ApplyLatch&) = 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).
|
|
constexpr int kLanesDisplayAsNormal = 2;
|
|
|
|
// C_LANESETTINGS &32 = hide per-lane buttons; OR'd in, never clobbering the
|
|
// mask. Deliberately NOT setting &1 (auto-remove empty lanes): the lazy-mint
|
|
// decision never mints an empty lane, so &1 buys nothing and risks REAPER
|
|
// silently removing a managed lane out from under the ownership index.
|
|
constexpr int kLaneSettingsHideButtons = 32;
|
|
|
|
// Makes a tool-split track's display read as an ordinary track. Gated by every
|
|
// caller on the tool-driven transition INTO fixed lanes (freeMode != 2 before
|
|
// the flip) — a track already in fixed-lane mode (the user's own) never
|
|
// reaches this, so a user's comp-lane display prefs are never stomped.
|
|
void applyTransparentLaneDisplay(MediaTrack* tr) {
|
|
SetMediaTrackInfo_Value(tr, "C_LANESCOLLAPSED",
|
|
static_cast<double>(kLanesDisplayAsNormal));
|
|
const int settings = static_cast<int>(GetMediaTrackInfo_Value(tr, "C_LANESETTINGS"));
|
|
SetMediaTrackInfo_Value(tr, "C_LANESETTINGS",
|
|
static_cast<double>(settings | kLaneSettingsHideButtons));
|
|
}
|
|
|
|
// The master track is not enumerated by GetTrack (index space excludes it),
|
|
// so it can never enter the tree — the master-untouched invariant holds by
|
|
// construction.
|
|
std::vector<TrackFolderEntry> readFolderEntries(
|
|
ReaProject* proj,
|
|
TrackHandles& handleByGuid) {
|
|
std::vector<TrackFolderEntry> entries;
|
|
int count = CountTracks(proj);
|
|
entries.reserve(static_cast<std::size_t>(count));
|
|
handleByGuid.reserve(static_cast<std::size_t>(count));
|
|
for (int i = 0; i < count; ++i) {
|
|
MediaTrack* tr = GetTrack(proj, i);
|
|
if (!tr) continue;
|
|
std::string guid = guidString(tr);
|
|
if (guid.empty()) continue;
|
|
int depth = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH"));
|
|
entries.push_back(TrackFolderEntry{guid, depth});
|
|
handleByGuid.emplace_back(guid, tr);
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Lane `laneIdx`'s durable name (P_LANENAME:n) on `tr`, or empty if unnamed /
|
|
// unavailable (non-fixed-lane track).
|
|
std::string laneName(MediaTrack* tr, int laneIdx) {
|
|
char parm[32];
|
|
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
|
char buf[512] = {0};
|
|
if (!GetSetMediaTrackInfo_String(tr, parm, buf, false)) return {};
|
|
return std::string(buf);
|
|
}
|
|
|
|
// Managed lane durable key -> current ordinal on `tr`. Manual lanes are
|
|
// omitted, so a key absent from the map must not be driven.
|
|
std::map<std::string, int> managedLaneOrdinals(MediaTrack* tr) {
|
|
std::map<std::string, int> byKey;
|
|
const int numLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
|
for (int lane = 0; lane < numLanes; ++lane) {
|
|
std::optional<std::string> key = managedLaneKey(laneName(tr, lane));
|
|
if (key) byKey.emplace(*key, lane); // first ordinal wins if names collide
|
|
}
|
|
return byKey;
|
|
}
|
|
|
|
// Track-side C_LANEPLAYS:N alone hides+silences every item on lane N (SDK:
|
|
// item-side C_LANEPLAYS is read-only, so no per-item write exists or is
|
|
// needed). B_FIXEDLANE_HIDDEN is also read-only — hide/show follows from
|
|
// C_LANEPLAYS=0/1, never written directly.
|
|
bool applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) {
|
|
char parm[32];
|
|
std::snprintf(parm, sizeof(parm), "C_LANEPLAYS:%d", laneIdx);
|
|
return writeIfChanged(tr, parm, static_cast<double>(lanePlays));
|
|
}
|
|
|
|
// `freeMode` alone decides whether UpdateTimeline is owed; `wrote` is any project
|
|
// write at all, which is what the apply's undo-point decision reads.
|
|
struct LaneApplyResult {
|
|
bool freeMode = false;
|
|
bool wrote = false;
|
|
};
|
|
|
|
// Groups ops by track, reconciles each op's durable laneKey to the track's
|
|
// current ordinal (a stale/renamed/deleted key is pruned, never mis-driven),
|
|
// enables fixed-lane mode on any track carrying a managed lane, and drives
|
|
// C_LANEPLAYS. UpdateTimeline() is the caller's job when `freeMode` comes back
|
|
// true (SDK: required after an I_FREEMODE change).
|
|
LaneApplyResult applyLaneOps(const TrackByGuid& handleByGuid,
|
|
const std::vector<LanePlayOp>& lanes) {
|
|
LaneApplyResult out;
|
|
if (lanes.empty()) return out;
|
|
|
|
std::map<std::string, std::vector<const LanePlayOp*>> byTrack;
|
|
for (const LanePlayOp& op : lanes) byTrack[op.trackGuid].push_back(&op);
|
|
|
|
for (const auto& [guid, ops] : byTrack) {
|
|
MediaTrack* tr = resolve(handleByGuid, guid);
|
|
if (!tr) continue; // stale GUID — prune
|
|
|
|
// Every track reaching here already owns a managed lane (planToggle
|
|
// only emits ops for managed lanes), so re-asserting fixed-lane mode
|
|
// is always a tool-driven (re)split — never a user's untouched
|
|
// manual-fixed-lane track — and gets the same transparent display.
|
|
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
|
if (freeMode != kFreeModeFixedLanes) {
|
|
SetMediaTrackInfo_Value(tr, "I_FREEMODE",
|
|
static_cast<double>(kFreeModeFixedLanes));
|
|
applyTransparentLaneDisplay(tr);
|
|
out.freeMode = true;
|
|
out.wrote = true;
|
|
}
|
|
|
|
const std::map<std::string, int> ordinals = managedLaneOrdinals(tr);
|
|
for (const LanePlayOp* op : ops) {
|
|
auto it = ordinals.find(op->laneKey);
|
|
if (it == ordinals.end()) continue; // key not live on this track — prune
|
|
if (applyLanePlays(tr, it->second, op->lanePlays)) out.wrote = true;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// One pass, so the assign loop needs no per-item re-scan.
|
|
std::map<std::string, MediaItem*> itemHandlesByGuid(MediaTrack* tr) {
|
|
std::map<std::string, MediaItem*> byGuid;
|
|
const int itemCount = CountTrackMediaItems(tr);
|
|
for (int i = 0; i < itemCount; ++i) {
|
|
MediaItem* it = GetTrackMediaItem(tr, i);
|
|
if (!it) continue;
|
|
std::string ig = itemGuid(it);
|
|
if (!ig.empty()) byGuid.emplace(std::move(ig), it);
|
|
}
|
|
return byGuid;
|
|
}
|
|
|
|
// An untagged item is Arrange by default (mirrors leafBelongsToMode). A
|
|
// show-both/multi-mode item resolves to its first mode id — unusual for lane
|
|
// content, and any one mode is sufficient for the decision.
|
|
std::string itemModeFromMembership(const ViewModeModel& model, const std::string& itemGuid) {
|
|
const std::set<std::string> modes = model.membership().modesOf(itemGuid);
|
|
if (modes.empty()) return kArrangeModeId;
|
|
return *modes.begin();
|
|
}
|
|
|
|
// The per-track LaneItem picture the pure decision consumes. Manual-lane reads
|
|
// are skipped on a non-fixed-lane track (isOnManualLane is false there anyway).
|
|
std::vector<LaneTrack> readLaneTracks(
|
|
const ViewModeModel& model,
|
|
const TrackHandles& handleByGuid) {
|
|
std::vector<LaneTrack> tracks;
|
|
tracks.reserve(handleByGuid.size());
|
|
for (const auto& [guid, tr] : handleByGuid) {
|
|
if (!tr) continue; // TrackHandles legally carries nulls post-validation (view.cpp)
|
|
LaneTrack lt;
|
|
lt.trackGuid = guid;
|
|
|
|
const bool fixedLane =
|
|
static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes;
|
|
|
|
const int itemCount = CountTrackMediaItems(tr);
|
|
lt.items.reserve(static_cast<std::size_t>(itemCount));
|
|
for (int i = 0; i < itemCount; ++i) {
|
|
MediaItem* it = GetTrackMediaItem(tr, i);
|
|
if (!it) continue;
|
|
const std::string ig = itemGuid(it);
|
|
if (ig.empty()) continue;
|
|
LaneItem li;
|
|
li.guid = ig;
|
|
li.modeId = itemModeFromMembership(model, ig);
|
|
const std::string ln = fixedLane ? itemLaneName(tr, it) : std::string{};
|
|
li.onManualLane = isOnManualLane(fixedLane, ln);
|
|
lt.items.push_back(std::move(li));
|
|
}
|
|
tracks.push_back(std::move(lt));
|
|
}
|
|
return tracks;
|
|
}
|
|
|
|
// Idempotent, and non-destructive: only this reversible flag is written, never
|
|
// a move in time or across tracks.
|
|
bool assignItemToLane(MediaTrack* tr, MediaItem* it, int laneOrdinal) {
|
|
const int current = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
|
if (current == laneOrdinal) return false; // already there — no-op
|
|
SetMediaItemInfo_Value(it, "I_FIXEDLANE", static_cast<double>(laneOrdinal));
|
|
return true;
|
|
}
|
|
|
|
// Applies the pure LaneMintPlan. Returns true if any project write actually
|
|
// changed state (⇒ caller keeps the Undo block and refreshes the timeline).
|
|
//
|
|
// The plan only ever names managed-prefixed lanes and only ever assigns
|
|
// 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 TrackByGuid& handleByGuid) {
|
|
bool changed = false;
|
|
|
|
std::map<std::string, std::vector<const LaneMint*>> mintsByTrack;
|
|
for (const LaneMint& m : plan.mints) mintsByTrack[m.trackGuid].push_back(&m);
|
|
std::map<std::string, std::vector<const LaneAssign*>> assignsByTrack;
|
|
for (const LaneAssign& a : plan.assigns) assignsByTrack[a.trackGuid].push_back(&a);
|
|
|
|
for (const LaneMintPlan::TrackSplit& split : plan.splits) {
|
|
MediaTrack* tr = resolve(handleByGuid, split.trackGuid);
|
|
if (!tr) continue; // stale GUID — prune
|
|
|
|
// A track not already in fixed-lane mode is one the tool is splitting
|
|
// now, so it owns the display; a track already at I_FREEMODE==2 (the
|
|
// user's own, or a prior tool run) skips this and keeps its display prefs.
|
|
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
|
if (freeMode != kFreeModeFixedLanes) {
|
|
SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast<double>(kFreeModeFixedLanes));
|
|
applyTransparentLaneDisplay(tr);
|
|
changed = true;
|
|
}
|
|
|
|
// Grow-only: a track may already carry the user's manual lanes, so the
|
|
// lane count only ever increases; managed lanes occupy the tail ordinals.
|
|
int laneCount = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
|
|
|
std::map<std::string, int> present = managedLaneOrdinals(tr);
|
|
|
|
for (const LaneMint* m : mintsByTrack[split.trackGuid]) {
|
|
model.lanes().setManaged(m->trackGuid, m->laneKey, m->modeId); // ownership
|
|
if (present.count(m->laneKey)) continue; // already minted — idempotent
|
|
|
|
const int laneIdx = laneCount++;
|
|
SetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES", static_cast<double>(laneCount));
|
|
char parm[32];
|
|
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
|
std::vector<char> name(m->laneKey.begin(), m->laneKey.end());
|
|
name.push_back('\0');
|
|
GetSetMediaTrackInfo_String(tr, parm, name.data(), true);
|
|
present.emplace(m->laneKey, laneIdx); // now resolvable for the assign pass
|
|
changed = true;
|
|
}
|
|
|
|
const std::map<std::string, int> ordinals = managedLaneOrdinals(tr);
|
|
const std::map<std::string, MediaItem*> itemsByGuid = itemHandlesByGuid(tr);
|
|
for (const LaneAssign* a : assignsByTrack[split.trackGuid]) {
|
|
auto ord = ordinals.find(a->laneKey);
|
|
if (ord == ordinals.end()) continue; // key not live — prune, never mis-assign
|
|
auto handle = itemsByGuid.find(a->itemGuid);
|
|
if (handle == itemsByGuid.end()) continue; // stale item GUID — prune
|
|
if (assignItemToLane(tr, handle->second, ord->second)) changed = true;
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool transportBlocksModeSwitch(ReaProject* proj) {
|
|
return (GetPlayStateEx(proj) & kTransportMoving) != 0;
|
|
}
|
|
|
|
bool modeApplyInProgress() { return g_applying; }
|
|
|
|
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
|
if (g_applying) return false; // see ApplyLatch; same fail-closed shape as the guards below
|
|
ApplyLatch latch;
|
|
|
|
if (!model.modes().contains(targetModeId)) {
|
|
return false; // reject before touching the project — no partial apply
|
|
}
|
|
|
|
// Pinned ONCE: begin and end sit a whole apply apart with the pump premise between
|
|
// them, so resolving "current project" independently at each would let a tab switch
|
|
// mid-apply open the block on one project and close it on another.
|
|
ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0);
|
|
if (!ValidatePtr2(nullptr, project, "ReaProject*")) return false;
|
|
|
|
// The discriminator behind both halves of the contract in view.h. A gate placed
|
|
// unconditionally here would break tagging and the project-load reapply during
|
|
// playback; a solo round on every reapply would flicker the user's solo on every
|
|
// membership edit.
|
|
const std::string outgoingModeId = model.activeModeId();
|
|
const bool realSwitch = targetModeId != outgoingModeId;
|
|
|
|
if (realSwitch && transportBlocksModeSwitch(project)) {
|
|
return false; // same fail-closed shape as the mode-exists guard above
|
|
}
|
|
|
|
TrackHandles handleByGuid;
|
|
std::vector<TrackFolderEntry> entries = readFolderEntries(project, handleByGuid);
|
|
TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
|
FolderTree tree = buildFolderTree(entries);
|
|
|
|
// Prune snapshots for tracks no longer in the live enumeration before
|
|
// planning (membership is intentionally left alone — see model.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);
|
|
|
|
// Read before any write, and while activeModeId() still names the mode being left.
|
|
const std::map<std::string, int> outgoingSolo =
|
|
realSwitch ? view::soloedTracks(readTrackSolos(handleByGuid))
|
|
: std::map<std::string, int>{};
|
|
|
|
TogglePlan plan = model.planToggle(tree, targetModeId);
|
|
|
|
// Target is guaranteed registered (checked at entry); fall back to the id
|
|
// defensively if that ever changes.
|
|
const Mode* targetMode = model.modes().query(targetModeId);
|
|
const std::string undoLabel =
|
|
"ReaSampler: activate " +
|
|
(targetMode ? targetMode->displayName : targetModeId) + " view";
|
|
|
|
int undoMask = kApplyUndoMask;
|
|
bool laneModeChanged = false;
|
|
bool wrote = false; // anything at all this apply changed — see applyMintsUndoPoint
|
|
std::vector<std::string> refusedParkNames;
|
|
FxRestoreDrops fxDrops;
|
|
int fxDropTracks = 0;
|
|
|
|
Undo_BeginBlock2(project);
|
|
{
|
|
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 / RESTORE: view_fx_park owns one track's whole move, flags and
|
|
// per-FX offline together. A refusal is named HERE, while the handle is
|
|
// fresh — never held past Undo_EndBlock2/TrackList_AdjustWindows.
|
|
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);
|
|
// Re-validated per track, not once at resolve: offlining a plugin is
|
|
// ASSUMED to pump the message loop, so a track deleted during an
|
|
// earlier iteration's FX writes would leave this handle dangling.
|
|
if (!tr || !trackStillLive(project, tr)) continue; // stale/deleted GUID — prune
|
|
|
|
const TrackApplyResult r = parkTrack(model, guid, tr, tp.flags);
|
|
if (r.refused) { refusedParkNames.push_back(trackDisplayName(tr)); continue; }
|
|
if (r.wrote) wrote = true;
|
|
if (r.fxMoved) undoMask |= UNDO_STATE_FX;
|
|
}
|
|
|
|
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 || !trackStillLive(project, tr)) continue; // stale/deleted GUID — prune
|
|
|
|
const TrackApplyResult r = restoreTrack(model, guid, tr, tp.fxOffline, tp.flags);
|
|
if (r.wrote) wrote = true;
|
|
if (r.fxMoved) undoMask |= UNDO_STATE_FX;
|
|
if (r.drops.total() > 0) { fxDrops.add(r.drops); ++fxDropTracks; }
|
|
}
|
|
|
|
// The FX writes above are the only thing here assumed to pump the message
|
|
// loop, so this is the one point a handle taken before them can have died.
|
|
// Re-validated for the whole enumeration rather than per consumer: lane ops,
|
|
// parent visibility and the solo replay all write through pre-FX handles.
|
|
for (auto& kv : handleByGuid) {
|
|
if (trackStillLive(project, kv.second)) continue;
|
|
trackByGuid.erase(kv.first);
|
|
kv.second = nullptr; // view_solo's writers already skip a null handle
|
|
}
|
|
|
|
// MANAGED LANES: the active mode's lane plays+shows, every other managed
|
|
// lane is silenced+hidden. Empty on a D1-only project — byte-identical there.
|
|
const LaneApplyResult lanes = applyLaneOps(trackByGuid, plan.lanes);
|
|
laneModeChanged = lanes.freeMode;
|
|
if (lanes.wrote) wrote = true;
|
|
|
|
// 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;
|
|
if (writeIfChanged(tr, "B_SHOWINTCP", show)) wrote = true;
|
|
if (writeIfChanged(tr, "B_SHOWINMIXER", show)) wrote = true;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Force REAPER to rebuild the TCP/MCP now rather than on the next user
|
|
// interaction — AdjustWindows(false) is the full relayout tracks appearing
|
|
// and disappearing owes.
|
|
TrackList_AdjustWindows(false);
|
|
UpdateArrange();
|
|
|
|
// UpdateTimeline() is owed only when a track was actually toggled into
|
|
// fixed lanes this apply (SDK requirement for I_FREEMODE changes).
|
|
if (laneModeChanged) UpdateTimeline();
|
|
|
|
const bool mint = applyMintsUndoPoint(realSwitch, wrote);
|
|
Undo_EndBlock2(project, mint ? undoLabel.c_str() : "", mint ? undoMask : 0);
|
|
|
|
// After the undo block and the UI hold: a report is not a project write, and
|
|
// it must not join what a Ctrl-Z rolls back.
|
|
reportRefusedParks(project, refusedParkNames);
|
|
reportFxRestoreDrops(fxDrops, fxDropTracks);
|
|
return true;
|
|
}
|
|
|
|
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
|
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.
|
|
const FolderTree tree = buildFolderTree(entries);
|
|
|
|
const std::vector<LaneTrack> tracks = readLaneTracks(model, handleByGuid);
|
|
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);
|
|
|
|
// `proj` deliberately NOT pinned the way applyMode pins it: nothing between this
|
|
// begin and its end pumps the message loop, so no tab switch can land between them.
|
|
Undo_BeginBlock2(proj);
|
|
const bool changed = applyMintPlan(model, plan, trackByGuid);
|
|
|
|
if (!changed) {
|
|
// Plan was non-empty but every write was already satisfied — discard
|
|
// the empty undo point rather than flooding history every detect tick.
|
|
Undo_EndBlock2(proj, "", 0);
|
|
|
|
// The arrange still needs a redraw: this no-op path is reached when a
|
|
// freshly-inserted item already landed on the active mode's playing
|
|
// lane (REAPER places new items on the playing lane), so
|
|
// assignItemToLane wrote nothing even though the item needs to appear
|
|
// there now. UpdateArrange() alone (no I_FREEMODE change happened, so
|
|
// UpdateTimeline isn't owed) is a repaint, not a mutation — stays
|
|
// outside the undo block.
|
|
UpdateArrange();
|
|
return false;
|
|
}
|
|
|
|
// Reapply the active mode's lane visibility so freshly-minted lanes take
|
|
// their play/show state immediately. applyMode is deliberately NOT reused
|
|
// 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(trackByGuid, togglePlan.lanes);
|
|
|
|
UpdateTimeline(); // a split happened this call — refresh is owed
|
|
UpdateArrange();
|
|
|
|
// 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
|
|
// (kApplyUndoMask, whose [verify — DAW] on lane-property domain binds here too).
|
|
Undo_EndBlock2(proj, "ReaSampler: separate cross-mode content into lanes", kApplyUndoMask);
|
|
return true;
|
|
}
|
|
|
|
void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
|
TrackHandles handleByGuid;
|
|
readFolderEntries(proj, handleByGuid); // populates handleByGuid (tree unused here)
|
|
|
|
// Pure read of REAPER state (no lane created, no I_FREEMODE/I_NUMFIXEDLANES/
|
|
// I_FIXEDLANE written) plus an ownership-index write, recovering managed
|
|
// classification from the durable name. An unprefixed lane is left alone.
|
|
for (const auto& [guid, tr] : handleByGuid) {
|
|
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
|
if (freeMode != kFreeModeFixedLanes) continue; // no fixed lanes ⇒ nothing managed
|
|
|
|
const int numLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
|
for (int lane = 0; lane < numLanes; ++lane) {
|
|
const std::string name = laneName(tr, lane);
|
|
std::optional<std::string> key = managedLaneKey(name);
|
|
if (!key) continue; // manual/unnamed lane — leave off the index
|
|
std::optional<std::string> mode = modeIdFromLaneName(name);
|
|
if (!mode) continue; // prefix-only/illegal name — skip defensively
|
|
|
|
// A mode id encoded in the name may no longer be registered (e.g.
|
|
// removed since the project was saved). Recording it managed would
|
|
// make the toggle planner drive a lane keyed to a mode that can
|
|
// never be active — permanently silenced, orphaning its items. So
|
|
// skip: the lane stays off the index (manual-by-default) but keeps
|
|
// its name, so a later re-registration of the mode heals cleanly.
|
|
if (!model.modes().contains(*mode)) continue;
|
|
model.lanes().setManaged(guid, *key, *mode);
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace reasampler
|