Design View: expose a park-shaped no-record track visible instead of adopting or refusing it
Adopting silently wipes hand repairs or hides untouched tracks forever; refusing was rejected too. EXPOSE hands the track back and self-heals on the next apply.
This commit is contained in:
+10
-1
@@ -114,6 +114,7 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
|
||||
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);
|
||||
{
|
||||
@@ -133,7 +134,11 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
const std::string& guid = tp.flags.front().guid;
|
||||
MediaTrack* tr = resolve(trackByGuid, guid);
|
||||
if (!tr) continue; // stale/deleted GUID — prune
|
||||
if (parkTrack(model, guid, tr, tp.flags)) wrote = true;
|
||||
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) {
|
||||
@@ -189,6 +194,10 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
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);
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
|
||||
#include "shell/view/view_park.h"
|
||||
|
||||
#include <cstring> // strnlen — bounded read of GetTrackName's buffer
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_GetTrackName
|
||||
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||
#define REAPERAPI_WANT_TrackFX_GetCount
|
||||
#define REAPERAPI_WANT_TrackFX_GetFXGUID
|
||||
#define REAPERAPI_WANT_TrackFX_GetOffline
|
||||
@@ -73,6 +76,22 @@ bool applyFlags(MediaTrack* tr, const std::vector<TrackFlagOp>& flags) {
|
||||
return wrote;
|
||||
}
|
||||
|
||||
// The visible/active mirror of a park write: same four ops, value 1 instead of
|
||||
// whatever the plan says (always 0 for a park plan — see makeParkPlan). Reusing
|
||||
// the incoming park ops rather than minting a separate "expose plan" keeps the
|
||||
// two writes provably addressing the same four parameters.
|
||||
bool exposeFlags(MediaTrack* tr, const std::vector<TrackFlagOp>& parkFlags) {
|
||||
bool wrote = false;
|
||||
for (const TrackFlagOp& op : parkFlags)
|
||||
if (writeIfChanged(tr, trackFlagParm(op.flag), 1.0)) wrote = true;
|
||||
return wrote;
|
||||
}
|
||||
|
||||
// The last EXPOSE report, so an unchanged set is not reprinted on every reapply
|
||||
// the track hasn't yet self-healed from.
|
||||
ReaProject* g_reportedOwner = nullptr;
|
||||
std::vector<std::string> g_reportedExposed;
|
||||
|
||||
} // namespace
|
||||
|
||||
const int kViewUndoMask = UNDO_STATE_TRACKCFG | UNDO_STATE_ITEMS | UNDO_STATE_MISCCFG;
|
||||
@@ -83,11 +102,28 @@ bool writeIfChanged(MediaTrack* tr, const char* parm, double value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<TrackFlagOp>& flags) {
|
||||
bool wrote = false;
|
||||
TrackParkResult parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<TrackFlagOp>& flags) {
|
||||
TrackParkResult result;
|
||||
|
||||
if (!model.holdsSnapshotFlags(guid)) {
|
||||
const TrackSnapshot* prior = model.snapshot(guid);
|
||||
|
||||
// No record of ANY kind (see parkTrack's header doc for why prior == nullptr
|
||||
// is the right gate): the ambiguous case. A live chain already reading
|
||||
// park-shaped is EXPOSEd — handed back visible, no snapshot stored — rather
|
||||
// than adopted as the user's intent.
|
||||
if (!prior) {
|
||||
auto read = [tr](Flag f) {
|
||||
return static_cast<int>(GetMediaTrackInfo_Value(tr, trackFlagParm(f)));
|
||||
};
|
||||
if (chainReadsParked(flags, read)) {
|
||||
if (exposeFlags(tr, flags)) result.wrote = true;
|
||||
result.exposed = true;
|
||||
return result; // this cycle hands the track back; it does not park it
|
||||
}
|
||||
}
|
||||
|
||||
TrackSnapshot snap = snapshotTrack(tr);
|
||||
|
||||
// A restore hands the flags back immediately but leaves the chain offline
|
||||
@@ -100,7 +136,6 @@ bool parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
// — so the false arm is unreached. Kept anyway: it is the direct
|
||||
// statement of what this branch actually needs, and cheaper to keep true
|
||||
// than to re-derive if that guarantee ever loosens.
|
||||
const TrackSnapshot* prior = model.snapshot(guid);
|
||||
if (prior && prior->fxHeld && prior->fxKeying == FxKeying::Identity) {
|
||||
snap.fxOffline = unionFxOfflineOnRepark(prior->fxOffline, snap.fxOffline);
|
||||
snap.fxKeying = FxKeying::Identity;
|
||||
@@ -112,11 +147,11 @@ bool parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
snap.fxKeying = prior->fxKeying;
|
||||
}
|
||||
model.storeSnapshot(guid, snap);
|
||||
wrote = true;
|
||||
result.wrote = true;
|
||||
}
|
||||
|
||||
if (applyFlags(tr, flags)) wrote = true;
|
||||
return wrote;
|
||||
if (applyFlags(tr, flags)) result.wrote = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool restoreTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
@@ -126,4 +161,22 @@ bool restoreTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string trackDisplayName(MediaTrack* tr) {
|
||||
std::vector<char> buf(1024, '\0');
|
||||
if (!GetTrackName || !GetTrackName(tr, buf.data(), static_cast<int>(buf.size())))
|
||||
return "(name unavailable)";
|
||||
return std::string(buf.data(), strnlen(buf.data(), buf.size()));
|
||||
}
|
||||
|
||||
void reportExposedTracks(ReaProject* proj, const std::vector<std::string>& trackNames) {
|
||||
if (!shouldReport(proj == g_reportedOwner, g_reportedExposed, trackNames)) return;
|
||||
g_reportedOwner = proj;
|
||||
g_reportedExposed = trackNames;
|
||||
|
||||
const std::string msg = describeExposedTracks(trackNames);
|
||||
// "!SHOW:" so an unattended project-load reapply cannot force the console open
|
||||
// (reaper_plugin_functions.h:6536).
|
||||
if (!msg.empty()) ShowConsoleMsg(("!SHOW:" + msg).c_str());
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
+88
-12
@@ -12,6 +12,7 @@
|
||||
|
||||
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
|
||||
class MediaTrack;
|
||||
class ReaProject;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
@@ -50,6 +51,21 @@ inline std::vector<FxOfflineState> unionFxOfflineOnRepark(
|
||||
return merged;
|
||||
}
|
||||
|
||||
// Does the track already sit at every value `parkFlags` would write? Templated on
|
||||
// the read so live code compares as it reads (one call per Flag, no live-value
|
||||
// vector) while a test drives the same fold from a table. FX-offline is
|
||||
// deliberately NOT folded in: Wave 3 moved every per-FX write off this path onto
|
||||
// the reclaimer, so a chain with these four flags fine but an FX hand-offlined is
|
||||
// not evidence THIS path ever touched the track — only the flags it would itself
|
||||
// have driven are. An empty plan is evidence of nothing and answers false.
|
||||
template <class ReadFlag>
|
||||
bool chainReadsParked(const std::vector<TrackFlagOp>& parkFlags, ReadFlag readFlag) {
|
||||
if (parkFlags.empty()) return false;
|
||||
for (const TrackFlagOp& op : parkFlags)
|
||||
if (readFlag(op.flag) != op.value) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The undo-state domains both the mode-switch apply (view.cpp) and lane minting
|
||||
// (view_lanes.cpp) write: track config/routing, item lane assignment, and the
|
||||
// ext-state block (UNDO_STATE_MISCCFG covers extension state). NOT UNDO_STATE_ALL,
|
||||
@@ -71,21 +87,81 @@ extern const int kViewUndoMask;
|
||||
// Returns whether it wrote.
|
||||
bool writeIfChanged(MediaTrack* tr, const char* parm, double value);
|
||||
|
||||
// What one parkTrack call did. `wrote` is any project write at all — a flag write
|
||||
// or a record write — and feeds the apply's undo-point fold. `exposed` is true iff
|
||||
// this call handed the track back visible instead of parking or adopting it; the
|
||||
// caller (view.cpp) collects exposed tracks across the whole apply for the one
|
||||
// batched console report.
|
||||
struct TrackParkResult {
|
||||
bool wrote = false;
|
||||
bool exposed = false;
|
||||
};
|
||||
|
||||
// Parks one inactive leaf: capture the pre-park truth when the model does not
|
||||
// already hold it, then drive the flags. Returns whether anything moved — a project
|
||||
// write or the record.
|
||||
// already hold ANY record for it (holdsSnapshotFlags false AND model.snapshot ==
|
||||
// nullptr), then drive the flags.
|
||||
//
|
||||
// The record is the tool's own account of an act it performed, and it is the ONLY
|
||||
// account — nothing infers a park from a live value any more. That is what retired
|
||||
// the old refusal: a track that merely LOOKS parked while no record names it is a
|
||||
// track the tool did not park, so its values are the user's own and capturing them
|
||||
// is right. So the capture is owed exactly when the flag half is not held.
|
||||
// A record with a held FX half but no held flag half (a re-park landing right
|
||||
// after a restore, before the reclaimer catches up) is NOT the no-record case —
|
||||
// model.snapshot(guid) is non-null there, so it takes the existing union path
|
||||
// below, never the branch this comment is about. That is how EXPOSE is known not
|
||||
// to fire on a track the tool legitimately still holds a record for: EXPOSE is
|
||||
// gated on snapshot(guid) == nullptr, and a legitimately-held record is by
|
||||
// definition non-null (eraseIfSpent only drops a record once BOTH halves are
|
||||
// spent).
|
||||
//
|
||||
// A still-held FX half is the one thing not re-read: it outlives the flag half the
|
||||
// restore spends, the chain stays offline until the reclaimer onlines it, and
|
||||
// re-reading it there would file the park's own state as the user's.
|
||||
bool parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<TrackFlagOp>& flags);
|
||||
// The true no-record case splits on whether the live chain already reads
|
||||
// park-shaped (chainReadsParked, flags only — see its own comment for why FX is
|
||||
// excluded). Not park-shaped: the chain is the track's own honest state, captured
|
||||
// and parked exactly as ever. Park-shaped: parking now would adopt a park with no
|
||||
// account behind it — an undo/redo or reload that dropped the snapshot, a hand or
|
||||
// script edit, or simply a track the user keeps hidden — as the user's intent,
|
||||
// which either wipes a hand repair on the very next restore or hides an untouched
|
||||
// track for good. EXPOSE hands it back instead: no snapshot stored, the four
|
||||
// flags driven to visible/active, nothing reported here (the caller batches it).
|
||||
// The next apply sees an honest chain again and captures/parks it normally —
|
||||
// self-healing, no user action required.
|
||||
TrackParkResult parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<TrackFlagOp>& flags);
|
||||
|
||||
// GetTrackName, matching capture::trackName's reason for the same choice: "Track
|
||||
// N" for an unnamed track, matching REAPER's own convention. Exported so a caller
|
||||
// holding the handle only briefly (view.cpp's park loop, before
|
||||
// Undo_EndBlock2/TrackList_AdjustWindows can invalidate it) captures the name at
|
||||
// resolve time rather than the handle.
|
||||
std::string trackDisplayName(MediaTrack* tr);
|
||||
|
||||
// One console message for the tracks EXPOSEd this apply, empty when none. NAMES
|
||||
// them: an exposed track was hidden from both the TCP and the mixer moments ago,
|
||||
// so a bare count points at nothing the user can go and find. Says the tool lost
|
||||
// its own record and handed the track back — never "refused".
|
||||
inline std::string describeExposedTracks(const std::vector<std::string>& trackNames) {
|
||||
if (trackNames.empty()) return {};
|
||||
const bool one = trackNames.size() == 1;
|
||||
std::string msg = "ReaSampler: " + std::to_string(trackNames.size()) + " track" +
|
||||
(one ? "" : "s") + " handed back visible -- the tool's record of " +
|
||||
(one ? "its" : "their") +
|
||||
" pre-park state is gone (an undo/redo or a project reload dropped "
|
||||
"it), and parking now would have recorded the parked state as "
|
||||
"yours instead.\n";
|
||||
for (const std::string& name : trackNames) msg += " " + name + "\n";
|
||||
msg += "The next mode switch will park it correctly from what you see now.\n";
|
||||
return msg;
|
||||
}
|
||||
|
||||
// Pure gate behind reportExposedTracks' one-per-changed-set memo: true on any
|
||||
// change of owner or of the named set, false on an exact repeat.
|
||||
inline bool shouldReport(bool sameOwnerAsLast, const std::vector<std::string>& last,
|
||||
const std::vector<std::string>& now) {
|
||||
return !(sameOwnerAsLast && last == now);
|
||||
}
|
||||
|
||||
// Prints describeExposedTracks for `trackNames` (captured by the caller while its
|
||||
// track handles are still fresh — see view.cpp's park loop), ONCE per changed set
|
||||
// per project: the same exposure recurs on every reapply until the track
|
||||
// self-heals, and repeating it verbatim is noise the user cannot act on any
|
||||
// better the second time. `proj` is used only as a memo key, never dereferenced.
|
||||
void reportExposedTracks(ReaProject* proj, const std::vector<std::string>& trackNames);
|
||||
|
||||
// Restores one returning leaf from its record and spends the record's flag half.
|
||||
// Always true: the record is consumed here, so this apply changed state and is not
|
||||
|
||||
Reference in New Issue
Block a user