226 lines
9.8 KiB
C++
226 lines
9.8 KiB
C++
// 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 <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "track_guid.h"
|
|
#include "view_tree.h"
|
|
|
|
#define REAPERAPI_MINIMAL
|
|
#define REAPERAPI_WANT_CountTracks
|
|
#define REAPERAPI_WANT_GetTrack
|
|
#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
|
|
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
|
#define REAPERAPI_WANT_UpdateArrange
|
|
#include "reaper_plugin_functions.h"
|
|
|
|
namespace reasampler {
|
|
|
|
namespace {
|
|
|
|
// 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 = 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;
|
|
}
|
|
|
|
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).
|
|
//
|
|
// HAZARD (deferred, PLAN "reconcile on delete/restructure"): the remap is by
|
|
// slot INDEX, not plugin identity. If the FX chain changed while the track was
|
|
// parked, snapshot slot k is restored onto whatever plugin now occupies slot k —
|
|
// the bounds-check guards against out-of-range, not against a reshuffled chain.
|
|
// Acceptable for D2; full identity-based reconciliation is future hardening.
|
|
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
|
|
|
|
// Snapshot ONCE, at the first park. If a snapshot already exists the track is
|
|
// still parked from a prior apply, and its live flags are the PARKED (hidden)
|
|
// values — recapturing here would overwrite the true pre-park state with zeros,
|
|
// so a later restore would restore the track to hidden and it would vanish for
|
|
// good. Re-applying the park flags to an already-parked track is idempotent and
|
|
// fine; only the snapshot must not be recaptured. 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 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);
|
|
}
|
|
|
|
// PARENT VISIBILITY (never parked): visibleTracks() marks a parent visible when
|
|
// a descendant leaf is visible in the target mode OR the parent belongs to the
|
|
// mode by its own membership (untagged folder → Arrange default). Recomputed
|
|
// every toggle rather than snapshotted. Drive only the two visibility flags;
|
|
// never touch B_MAINSEND/I_FXEN/FX-offline 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);
|
|
}
|
|
|
|
// Build the undo label from the ACTUAL target mode's display name, so activating
|
|
// Arrange doesn't leave an "activate Design view" undo point (and vice versa).
|
|
// The target is guaranteed registered (checked at entry), so query() is non-null;
|
|
// 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";
|
|
|
|
model.setActiveMode(targetModeId);
|
|
|
|
// Force REAPER to rebuild the TCP + MCP so visibility/park changes appear now,
|
|
// not on the user's next TCP interaction. TrackList_AdjustWindows(false) does the
|
|
// major (full) relayout required when tracks appear/disappear from the panels;
|
|
// UpdateArrange() repaints the arrange view. Both are documented for exactly this
|
|
// "you changed track-info flags, now refresh the panels" case.
|
|
TrackList_AdjustWindows(false);
|
|
UpdateArrange();
|
|
|
|
Undo_EndBlock2(proj, undoLabel.c_str(), -1);
|
|
return true;
|
|
}
|
|
|
|
} // namespace reasampler
|