Files
reasampler/src/shell/view/view.cpp
T

231 lines
9.9 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), one track's park/restore in view_park
// and the lane writes in view_lanes; this file owns the plan iteration, parent
// visibility, and the one undo block they all ride.
#include "shell/view/view.h"
#include <map>
#include <set>
#include <string>
#include <vector>
#include "core/view/solo_cache.h"
#include "core/view/view_tree.h"
#include "shell/view/fx_reclaim.h"
#include "shell/view/view_lanes.h"
#include "shell/view/view_park.h"
#include "shell/view/view_reconcile.h"
#include "shell/view/view_solo.h"
#include "shell/view/view_tracks.h"
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_EnumProjects
#define REAPERAPI_WANT_GetPlayStateEx
#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
#include "reaper_plugin_functions.h"
namespace reasampler {
using view::buildFolderTree;
using view::TrackFolderEntry;
namespace {
// GetPlayStateEx bitmask. SDK: &1 playing, &2 paused, &4 recording — paused is
// deliberately excluded, nothing is moving there.
constexpr int kTransportMoving = 1 | 4;
// applyMode is reachable from the load tick, the actions and render_in_place. Nothing
// it calls is known to pump the message loop now that the FX writes are gone, but a
// re-entrant apply would plan against the same model the outer loop is iterating and
// consume records out from under it — one dropped call is cheaper than that. 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;
};
} // namespace
bool transportBlocksModeSwitch(ReaProject* proj) {
return (GetPlayStateEx(proj) & kTransportMoving) != 0;
}
bool modeApplyInProgress() { return g_applying; }
namespace {
bool applyModeImpl(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: resolving "current project" independently at the begin and the end
// 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);
const TrackByGuid trackByGuid = indexHandles(handleByGuid);
FolderTree tree = buildFolderTree(entries);
// Prune records 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";
bool laneModeChanged = false;
bool wrote = false; // anything at all this apply changed — see applyMintsUndoPoint
std::vector<std::string> exposedNames; // named while handles are fresh, reported after the block
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_park owns one track's whole move. Flags only — the
// parked chain stays online until the reclaimer reaches 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/deleted GUID — prune
const TrackParkResult r = parkTrack(model, guid, tr, tp.flags);
if (r.wrote) wrote = true;
// Named here, not after the block: TrackList_AdjustWindows below can
// invalidate the handle.
if (r.exposed) exposedNames.push_back(trackDisplayName(tr));
}
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/deleted GUID — prune
if (restoreTrack(model, guid, tr, tp.flags)) wrote = true;
}
// 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 recorded.
// 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 ? kViewUndoMask : 0);
// After the undo block: an EXPOSE report is a console message, not a project
// write, and must not join what a Ctrl-Z rolls back.
reportExposedTracks(project, exposedNames);
// The tool's own account of what it just left driven — the only one that survives
// an undo rolling the model back underneath it.
noteAppliedState(project, model);
return true;
}
} // namespace
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
const bool applied = applyModeImpl(model, targetModeId, proj);
// Every membership, snapshot and active-mode mutation in the system reaches the
// project through this call, so this is the whole invalidation surface: the
// reclaimer's queue is a cache of what the FX chains SHOULD be, never state.
//
// Dropped on a REFUSAL too, and that is the load-bearing half. Callers mutate the
// model FIRST and reapply to make it visible (design_view_actions' tag/untag/
// show-both, render_in_place, the load tick), and every one of them discards this
// return — so a refusal is exactly the case where the mutation stands and nothing
// else would drop a queue that now converges toward the mode the user just left.
// The cost when the refusal followed no mutation is one re-derivation on the timer.
invalidateFxReclaim();
return applied;
}
} // namespace reasampler