231 lines
9.9 KiB
C++
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
|