feat(view): D2 view shell — apply mode toggle plan to live project
Reads I_FOLDERDEPTH into a FolderTree (pure view_tree helper, unit-tested), snapshots to-be-parked tracks before parking, runs the D1 planner, and applies park/restore flag + per-FX-offline writes under an undo block. Master and mute/solo untouched; acts only on tagged tracks.
This commit is contained in:
+188
@@ -0,0 +1,188 @@
|
||||
// view.cpp — REAPER-facing Design View shell (Phase D2). See view.h.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
||||
// pointers; here they are extern (CLAUDE.md §contract).
|
||||
//
|
||||
// The tree arithmetic (I_FOLDERDEPTH -> FolderTree) lives in the pure view_tree
|
||||
// module so it is unit-tested outside the DAW; this file owns only the REAPER
|
||||
// reads/writes and the snapshot-before-park ordering.
|
||||
|
||||
#include "view.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "view_tree.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountTracks
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#define REAPERAPI_WANT_GetTrackGUID
|
||||
#define REAPERAPI_WANT_guidToString
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_TrackFX_GetCount
|
||||
#define REAPERAPI_WANT_TrackFX_GetOffline
|
||||
#define REAPERAPI_WANT_TrackFX_SetOffline
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
// REAPER's GUID -> string form. guidToString needs a 64-byte destination (SDK
|
||||
// contract); the canonical "{XXXXXXXX-...}" string is the membership-index key the
|
||||
// actions layer tags with, so the tree keys and the model keys align exactly.
|
||||
std::string trackGuidString(MediaTrack* tr) {
|
||||
GUID* g = GetTrackGUID(tr);
|
||||
if (!g) return {};
|
||||
char buf[64] = {0};
|
||||
guidToString(g, buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// The parmname for each planner Flag. All four are documented bool*/int* track
|
||||
// info params driven through the double-valued Get/SetMediaTrackInfo_Value API.
|
||||
const char* flagParm(Flag f) {
|
||||
switch (f) {
|
||||
case Flag::ShowInTcp: return "B_SHOWINTCP";
|
||||
case Flag::ShowInMixer: return "B_SHOWINMIXER";
|
||||
case Flag::MainSend: return "B_MAINSEND";
|
||||
case Flag::FxEnable: return "I_FXEN";
|
||||
}
|
||||
return "B_SHOWINTCP"; // unreachable; keeps the compiler quiet
|
||||
}
|
||||
|
||||
// Reads the arrange-ordered track list and their I_FOLDERDEPTH, keyed by GUID.
|
||||
// The master track is NOT enumerated by GetTrack (index space is the non-master
|
||||
// tracks), so it can never enter the tree — the master-untouched invariant holds
|
||||
// by construction. Also caches the MediaTrack* per GUID so later apply steps
|
||||
// resolve a GUID back to its handle without a second linear scan.
|
||||
std::vector<TrackFolderEntry> readFolderEntries(
|
||||
ReaProject* proj,
|
||||
std::vector<std::pair<std::string, MediaTrack*>>& 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 = trackGuidString(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;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Captures a track's prior driven-flag state BEFORE it is parked. Reads only the
|
||||
// four owned flags + per-FX offline; never B_MUTE/I_SOLO, never the master (not
|
||||
// reachable here). ints preserve whatever REAPER reported (defensive per D1's
|
||||
// TrackSnapshot contract).
|
||||
TrackSnapshot snapshotTrack(MediaTrack* tr) {
|
||||
TrackSnapshot snap;
|
||||
snap.showInTcp = static_cast<int>(GetMediaTrackInfo_Value(tr, "B_SHOWINTCP"));
|
||||
snap.showInMixer = static_cast<int>(GetMediaTrackInfo_Value(tr, "B_SHOWINMIXER"));
|
||||
snap.mainSend = static_cast<int>(GetMediaTrackInfo_Value(tr, "B_MAINSEND"));
|
||||
snap.fxEnable = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FXEN"));
|
||||
|
||||
int fxCount = TrackFX_GetCount(tr);
|
||||
snap.fxOffline.reserve(static_cast<std::size_t>(fxCount));
|
||||
for (int fx = 0; fx < fxCount; ++fx) {
|
||||
snap.fxOffline.push_back(TrackFX_GetOffline(tr, fx) ? 1 : 0);
|
||||
}
|
||||
return snap;
|
||||
}
|
||||
|
||||
// Applies the planner's scalar-flag writes. B_* are bool* params, I_FXEN is int*,
|
||||
// all driven through the double API — marshal the plan's int value to double.
|
||||
void applyFlags(MediaTrack* tr, const std::vector<TrackFlagOp>& flags) {
|
||||
for (const TrackFlagOp& op : flags) {
|
||||
SetMediaTrackInfo_Value(tr, flagParm(op.flag), static_cast<double>(op.value));
|
||||
}
|
||||
}
|
||||
|
||||
// Parks a track's FX offline: the pure park plan leaves fxOffline empty by design;
|
||||
// the shell expands it from the live FX count and offlines every slot.
|
||||
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 — each slot back to its
|
||||
// captured value, never a blanket "online". Bounds-checked against the live FX
|
||||
// count in case the plugin chain changed while parked (prune-safe).
|
||||
void restoreFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& fxOffline) {
|
||||
int fxCount = TrackFX_GetCount(tr);
|
||||
for (const FxOfflineOp& op : fxOffline) {
|
||||
if (op.fxIndex < 0 || op.fxIndex >= fxCount) continue;
|
||||
TrackFX_SetOffline(tr, op.fxIndex, op.offline);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
||||
// Reject an unregistered target before touching the project (no partial apply).
|
||||
if (!model.modes().contains(targetModeId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
||||
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
|
||||
FolderTree tree = buildFolderTree(entries);
|
||||
|
||||
TogglePlan plan = model.planToggle(tree, targetModeId);
|
||||
|
||||
Undo_BeginBlock2(proj);
|
||||
|
||||
// PARK: snapshot BEFORE mutating, store into the model (so restore survives a
|
||||
// save-while-parked), then apply the park writes + expand the FX-offline loop.
|
||||
for (const TrackPlan& tp : plan.park) {
|
||||
// Every op in a TrackPlan targets the same track; take the guid from the
|
||||
// first flag op (the pure park plan always emits the four flag ops).
|
||||
if (tp.flags.empty()) continue;
|
||||
const std::string& guid = tp.flags.front().guid;
|
||||
MediaTrack* tr = resolve(handleByGuid, guid);
|
||||
if (!tr) continue; // stale GUID — prune
|
||||
|
||||
model.storeSnapshot(guid, snapshotTrack(tr));
|
||||
applyFlags(tr, tp.flags);
|
||||
parkFxOffline(tr);
|
||||
}
|
||||
|
||||
// RESTORE: apply the snapshot-sourced flag + per-FX offline writes verbatim,
|
||||
// then drop the now-consumed snapshot so a re-park recaptures fresh state.
|
||||
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);
|
||||
restoreFxOffline(tr, tp.fxOffline);
|
||||
model.clearSnapshot(guid);
|
||||
}
|
||||
|
||||
model.setActiveMode(targetModeId);
|
||||
|
||||
Undo_EndBlock2(proj, "ReaSampler: apply Design View mode", -1);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user