Make Design View membership edits undoable: the view_state write moves inside the block
All four actions share one body; the point turns on a model diff, so a no-op leaves history and the Undo button alone. The Save-As prompt stays outside.
This commit is contained in:
@@ -36,6 +36,18 @@ is owned by other directories and only skinned here.
|
|||||||
reclaim span is open; the mechanism and its compile-time enforcement are
|
reclaim span is open; the mechanism and its compile-time enforcement are
|
||||||
`src/shell/undo/CLAUDE.md`'s. Bank index verbs additionally stay one batched
|
`src/shell/undo/CLAUDE.md`'s. Bank index verbs additionally stay one batched
|
||||||
`UNDO_STATE_MISCCFG` point apiece, so one bank operation is one Ctrl-Z.
|
`UNDO_STATE_MISCCFG` point apiece, so one bank operation is one Ctrl-Z.
|
||||||
|
- **A Design View membership edit's undo point holds its `view_state` write — so the
|
||||||
|
persist runs INSIDE the block, and the point turns on a MODEL DIFF, not on "REAPER
|
||||||
|
was written to".** All four membership actions (tag, untag / tag→Arrange, show-both,
|
||||||
|
the item moves) share one body, `design_view_actions`' `runMembershipEdit`.
|
||||||
|
Everything the reapply they drive writes is the four driven track flags, which mint
|
||||||
|
no undo state at all (`src/shell/view/CLAUDE.md`), so a point closed BEFORE the
|
||||||
|
ext-state write covered no change: the edit was not undoable and rode whatever point
|
||||||
|
the user's next act minted. Two consequences bind. The persist's modal Save-As half
|
||||||
|
is split off and runs before the block opens — a modal may not hold a block open.
|
||||||
|
And a no-op edit records no label, because a recorded label re-titles the Undo button
|
||||||
|
whether or not REAPER appends an entry. Same shape as `shell/bank_ops`'
|
||||||
|
`persistBankOp`, for the same reason.
|
||||||
- **The prune action is the ONLY file-deletion action in the system**; it opens no
|
- **The prune action is the ONLY file-deletion action in the system**; it opens no
|
||||||
undo point (file deletion is not REAPER-undoable). It halts on
|
undo point (file deletion is not REAPER-undoable). It halts on
|
||||||
`blockedByTracking` and prints whichever blockers fired — the malformed ledger,
|
`blockedByTracking` and prints whichever blockers fired — the malformed ledger,
|
||||||
|
|||||||
@@ -104,16 +104,13 @@ std::vector<std::string> selectedTrackGuids() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reapplies the CURRENT active mode so a membership mutation takes visible effect
|
// Reapplies the CURRENT active mode so a membership mutation takes visible effect
|
||||||
// immediately (park/unpark/re-derive parents), answering whether it wrote any project
|
// immediately (park/unpark/re-derive parents). `applied` is deliberately discarded: a
|
||||||
// state — the caller's block mints only if something did. `applied` is deliberately
|
// refused reapply leaves the membership edit standing and unapplied, and the user's
|
||||||
// discarded: a refused reapply leaves the membership edit standing and unapplied, and
|
// next edit or mode switch reapplies it. What must NOT be dropped with it is the
|
||||||
// the user's next edit or mode switch reapplies it (a refusal writes nothing, so it
|
|
||||||
// reports false here and the block discards). What must NOT be dropped with it is the
|
|
||||||
// reclaimer's queue, which is why applyMode invalidates on its refusal path too
|
// reclaimer's queue, which is why applyMode invalidates on its refusal path too
|
||||||
// (view.cpp).
|
// (view.cpp).
|
||||||
bool reapplyActiveMode() {
|
void reapplyActiveMode() {
|
||||||
return applyMode(g_session->view(), g_session->view().activeModeId(), nullptr)
|
applyMode(g_session->view(), g_session->view().activeModeId(), nullptr);
|
||||||
.wroteProjectState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr int kFreeModeFixedLanes = 2; // I_FREEMODE value; mirrors the shell's constant
|
constexpr int kFreeModeFixedLanes = 2; // I_FREEMODE value; mirrors the shell's constant
|
||||||
@@ -147,33 +144,45 @@ std::vector<RetagItem> selectedRetagItems() {
|
|||||||
// serialization. Every other action keeps the full save.
|
// serialization. Every other action keeps the full save.
|
||||||
enum class PersistScope { Full, ViewOnly };
|
enum class PersistScope { Full, ViewOnly };
|
||||||
|
|
||||||
// Persists the bank + Design-View model after every state-changing action so the
|
// The persist's MODAL half, split from the write below so a caller can run the two on
|
||||||
// view model is not lost across save/close/reopen. If membership is non-empty and
|
// opposite sides of an undo block. If membership is non-empty and the project is
|
||||||
// the project is unsaved, prompts Save-As first (mirrors the flow capture uses) —
|
// unsaved, prompts Save-As (mirrors the flow capture uses) — DAW-ONLY:
|
||||||
// DAW-ONLY: Main_SaveProject(proj, true) blocks until the dialog is dismissed.
|
// Main_SaveProject(proj, true) blocks until the dialog is dismissed. False means the
|
||||||
void persistViewState(PersistScope scope) {
|
// user declined and nothing may be written.
|
||||||
if (!g_session->view().membership().empty()) {
|
bool ensurePersistTarget() {
|
||||||
|
if (g_session->view().membership().empty()) return true;
|
||||||
|
|
||||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||||
if (proj) {
|
if (!proj) return true; // no active project — the write below no-ops on its own
|
||||||
|
|
||||||
auto readRppPath = [&]() -> std::string {
|
auto readRppPath = [&]() -> std::string {
|
||||||
std::vector<char> buf(4096, '\0');
|
std::vector<char> buf(4096, '\0');
|
||||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||||
return std::string(buf.data());
|
return std::string(buf.data());
|
||||||
};
|
};
|
||||||
|
|
||||||
if (readRppPath().empty()) {
|
if (!readRppPath().empty()) return true;
|
||||||
|
|
||||||
Main_SaveProject(proj, true);
|
Main_SaveProject(proj, true);
|
||||||
if (readRppPath().empty()) { // still empty -> user cancelled
|
if (!readRppPath().empty()) return true; // still empty -> user cancelled
|
||||||
ShowConsoleMsg(
|
|
||||||
"ReaSampler: Design View state will not persist until "
|
ShowConsoleMsg("ReaSampler: Design View state will not persist until "
|
||||||
"the project is saved.\n");
|
"the project is saved.\n");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The persist's WRITE half: the bank + Design-View model, so neither is lost across
|
||||||
|
// save/close/reopen. Returns whether the write was ISSUED — false means no active
|
||||||
|
// project or an unsaved one, with NOTHING written (session.h owns that contract), which
|
||||||
|
// is the only reading a caller may turn into a discarded undo point.
|
||||||
|
bool writePersistedState(PersistScope scope) {
|
||||||
|
return scope == PersistScope::ViewOnly ? g_session->saveViewStateOnly()
|
||||||
|
: g_session->saveToActiveProject();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
// The two halves as a caller holding no undo block runs them.
|
||||||
if (scope == PersistScope::ViewOnly) g_session->saveViewStateOnly();
|
void persistViewState(PersistScope scope) {
|
||||||
else g_session->saveToActiveProject();
|
if (ensurePersistTarget()) writePersistedState(scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The footer segment already reads disabled while the transport runs, but an action can
|
// The footer segment already reads disabled while the transport runs, but an action can
|
||||||
@@ -211,17 +220,53 @@ void doActivateMode(const std::string& modeId) {
|
|||||||
bankPanelInvalidate(); // repaint the footer [Arrange|Design] toggle immediately
|
bankPanelInvalidate(); // repaint the footer [Arrange|Design] toggle immediately
|
||||||
}
|
}
|
||||||
|
|
||||||
// The block each membership-edit action below owns. The reapply they drive opens none
|
// The drive half of a membership edit: what runs inside the block, answering whether
|
||||||
// of its own — nothing it writes mints a point unaided — so a user-fired tag stays one
|
// it made a project change of its own that a point must hold even with nothing
|
||||||
// Ctrl-Z only because the ACTION spans it, and the label names the act the user
|
// persisted. The reapply alone never does — everything it writes is the four driven
|
||||||
// actually fired rather than the reapply it happened to trigger. Every one of them
|
// flags, which mint no undo state (src/shell/view/CLAUDE.md).
|
||||||
// closes the block before persistViewState, which can pop a modal Save-As.
|
bool driveReapply() {
|
||||||
|
reapplyActiveMode();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The item moves' drive: re-drive the same minting/apply path auto-tag uses — mint or
|
||||||
|
// split lanes for any track whose items now span modes, then reassert active-mode lane
|
||||||
|
// visibility. A mint moves items between lanes, which is a real project change whether
|
||||||
|
// or not the persist below lands.
|
||||||
|
bool driveMintLanesAndReapply() {
|
||||||
|
const bool minted = mintManagedLanes(g_session->view(), nullptr);
|
||||||
|
reapplyActiveMode();
|
||||||
|
return minted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ONE body behind all four membership edits (this directory's CLAUDE.md owns the
|
||||||
|
// rule; what follows is why the ORDER is what it is). `mutate` is a pure model edit —
|
||||||
|
// nothing in it reaches REAPER.
|
||||||
//
|
//
|
||||||
// The point is recorded only where the reapply actually wrote: re-tagging tracks that
|
// The prompt sits between the edit and the block because it is pinned on both sides:
|
||||||
// already sit in that mode writes nothing to the project, and the ("", 0) discard form
|
// its condition reads the POST-edit membership (a first-ever tag on an unsaved project
|
||||||
// UndoBlock closes on by default is real from a hand-fired action (DAW-measured,
|
// must still prompt), and a modal may not stand inside an open block. The persist then
|
||||||
// src/shell/view/CLAUDE.md). The membership move itself is never in the point either
|
// runs INSIDE, being the only thing here a point can hold; a re-tag of tracks already
|
||||||
// way — persistViewState runs after the close.
|
// in that mode re-writes byte-identical ext state, which is why the label turns on the
|
||||||
|
// model diff and not on the write.
|
||||||
|
template <class Mutate>
|
||||||
|
void runMembershipEdit(Mutate&& mutate, bool (*drive)(), const std::string& label,
|
||||||
|
int mask) {
|
||||||
|
const MembershipIndex before = g_session->view().membership();
|
||||||
|
mutate();
|
||||||
|
const bool changed = !(g_session->view().membership() == before);
|
||||||
|
|
||||||
|
const bool persistable = ensurePersistTarget();
|
||||||
|
|
||||||
|
UndoBlock undo(nullptr);
|
||||||
|
const bool structural = drive();
|
||||||
|
const bool persisted = persistable && writePersistedState(PersistScope::Full);
|
||||||
|
if (structural || (changed && persisted))
|
||||||
|
undo.recordPoint(label, mask);
|
||||||
|
// Otherwise the block closes on its ("", 0) discard form — real from a hand-fired
|
||||||
|
// action (DAW-measured, src/shell/view/CLAUDE.md) — leaving history and the Undo
|
||||||
|
// button both untouched.
|
||||||
|
}
|
||||||
|
|
||||||
// tag() replaces any prior single-mode membership (a leaf lives in one mode; the
|
// tag() replaces any prior single-mode membership (a leaf lives in one mode; the
|
||||||
// cross-mode case is show-both).
|
// cross-mode case is show-both).
|
||||||
@@ -229,13 +274,11 @@ void doTag(const std::string& modeId) {
|
|||||||
const std::vector<std::string> guids = selectedTrackGuids();
|
const std::vector<std::string> guids = selectedTrackGuids();
|
||||||
if (guids.empty()) return; // nothing selected — no act, no undo point (cf. doMoveItems)
|
if (guids.empty()) return; // nothing selected — no act, no undo point (cf. doMoveItems)
|
||||||
|
|
||||||
{
|
runMembershipEdit(
|
||||||
UndoBlock undo(nullptr);
|
[&] {
|
||||||
for (const std::string& g : guids) g_session->view().membership().tag(g, modeId);
|
for (const std::string& g : guids) g_session->view().membership().tag(g, modeId);
|
||||||
if (reapplyActiveMode())
|
},
|
||||||
undo.recordPoint("ReaSampler: tag selected tracks -> " + modeId, kViewUndoMask);
|
&driveReapply, "ReaSampler: tag selected tracks -> " + modeId, kViewUndoMask);
|
||||||
}
|
|
||||||
persistViewState(PersistScope::Full);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared body behind "Untag selected" and "Tag -> Arrange" — Arrange is the absence
|
// Shared body behind "Untag selected" and "Tag -> Arrange" — Arrange is the absence
|
||||||
@@ -244,13 +287,11 @@ void doUntag() {
|
|||||||
const std::vector<std::string> guids = selectedTrackGuids();
|
const std::vector<std::string> guids = selectedTrackGuids();
|
||||||
if (guids.empty()) return;
|
if (guids.empty()) return;
|
||||||
|
|
||||||
{
|
runMembershipEdit(
|
||||||
UndoBlock undo(nullptr);
|
[&] {
|
||||||
for (const std::string& g : guids) g_session->view().membership().untag(g);
|
for (const std::string& g : guids) g_session->view().membership().untag(g);
|
||||||
if (reapplyActiveMode())
|
},
|
||||||
undo.recordPoint("ReaSampler: untag selected tracks", kViewUndoMask);
|
&driveReapply, "ReaSampler: untag selected tracks", kViewUndoMask);
|
||||||
}
|
|
||||||
persistViewState(PersistScope::Full);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flips each track's pin independently — the honest semantics of a toggle on a
|
// Flips each track's pin independently — the honest semantics of a toggle on a
|
||||||
@@ -259,14 +300,12 @@ void doShowBoth() {
|
|||||||
const std::vector<std::string> guids = selectedTrackGuids();
|
const std::vector<std::string> guids = selectedTrackGuids();
|
||||||
if (guids.empty()) return;
|
if (guids.empty()) return;
|
||||||
|
|
||||||
{
|
runMembershipEdit(
|
||||||
UndoBlock undo(nullptr);
|
[&] {
|
||||||
MembershipIndex& m = g_session->view().membership();
|
MembershipIndex& m = g_session->view().membership();
|
||||||
for (const std::string& g : guids) m.setShowBoth(g, !m.isShowBoth(g));
|
for (const std::string& g : guids) m.setShowBoth(g, !m.isShowBoth(g));
|
||||||
if (reapplyActiveMode())
|
},
|
||||||
undo.recordPoint("ReaSampler: toggle show-both on selected tracks", kViewUndoMask);
|
&driveReapply, "ReaSampler: toggle show-both on selected tracks", kViewUndoMask);
|
||||||
}
|
|
||||||
persistViewState(PersistScope::Full);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retag the current ITEM selection to `targetMode` (empty => untag -> Arrange
|
// Retag the current ITEM selection to `targetMode` (empty => untag -> Arrange
|
||||||
@@ -281,30 +320,20 @@ void doMoveItems(const std::string& targetMode) {
|
|||||||
const std::vector<ItemRetagOp> ops = planItemRetag(selected, targetMode);
|
const std::vector<ItemRetagOp> ops = planItemRetag(selected, targetMode);
|
||||||
if (ops.empty()) return; // nothing selected, or every selected item was exempt/empty
|
if (ops.empty()) return; // nothing selected, or every selected item was exempt/empty
|
||||||
|
|
||||||
|
runMembershipEdit(
|
||||||
|
[&] {
|
||||||
MembershipIndex& membership = g_session->view().membership();
|
MembershipIndex& membership = g_session->view().membership();
|
||||||
|
|
||||||
{
|
|
||||||
UndoBlock undo(nullptr);
|
|
||||||
for (const ItemRetagOp& op : ops) {
|
for (const ItemRetagOp& op : ops) {
|
||||||
if (op.untag) membership.untag(op.guid);
|
if (op.untag) membership.untag(op.guid);
|
||||||
else membership.tag(op.guid, op.modeId);
|
else membership.tag(op.guid, op.modeId);
|
||||||
}
|
}
|
||||||
// Re-drive the same minting/apply path auto-tag uses: mint/split lanes for any
|
},
|
||||||
// track whose items now span modes, then reassert active-mode lane visibility.
|
&driveMintLanesAndReapply,
|
||||||
// Either half writing is enough to mint; a retag that split no lanes and drove
|
targetMode.empty() ? std::string("ReaSampler: untag selected items")
|
||||||
// no flag leaves nothing for a point to hold.
|
|
||||||
const bool minted = mintManagedLanes(g_session->view(), nullptr);
|
|
||||||
const bool reapplied = reapplyActiveMode();
|
|
||||||
if (minted || reapplied)
|
|
||||||
undo.recordPoint(targetMode.empty()
|
|
||||||
? std::string("ReaSampler: untag selected items")
|
|
||||||
: "ReaSampler: move selected items -> " + targetMode,
|
: "ReaSampler: move selected items -> " + targetMode,
|
||||||
-1);
|
-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
persistViewState(PersistScope::Full);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void designViewRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) {
|
void designViewRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) {
|
||||||
|
|||||||
+28
-18
@@ -94,14 +94,18 @@ decide membership or mode rules.
|
|||||||
because a block there could only manufacture the point a project open or a membership
|
because a block there could only manufacture the point a project open or a membership
|
||||||
edit must not cost. This is why the same `I_FREEMODE` write is blocked at one site and
|
edit must not cost. This is why the same `I_FREEMODE` write is blocked at one site and
|
||||||
not the other: minting SPLITS a track, `applyLaneOps` only re-states which lane plays.
|
not the other: minting SPLITS a track, `applyLaneOps` only re-states which lane plays.
|
||||||
Second clause, binding every site that does open one: **a block that wrote nothing
|
Second clause, binding every site that does open one: **a block with nothing REAPER
|
||||||
closes on the discard form** (`Undo_EndBlock2(proj, "", 0)` — real from a hand-fired
|
can hold closes on the discard form** (`Undo_EndBlock2(proj, "", 0)` — real from a
|
||||||
action, per the measurement above), so re-tagging tracks already in that mode leaves
|
hand-fired action, per the measurement above), so re-tagging tracks already in that
|
||||||
no empty point. The one exception is a REAL SWITCH, which mints written state or not:
|
mode leaves no empty point and no relabelled Undo button. The one exception is a REAL
|
||||||
it is an explicitly fired action and has to stay undoable even where the plan found
|
SWITCH, which mints written state or not: it is an explicitly fired action and has to
|
||||||
nothing to write. "Wrote" means a REAPER write throughout — model moves persist
|
stay undoable even where the plan found nothing to write. "Nothing REAPER can hold" is
|
||||||
through `persistViewState`, outside every block, so no point could carry them
|
NOT "no REAPER write", and reading it that way was a shipped defect: the four driven
|
||||||
(`ModeApplyResult`, `view.h`).
|
flags ARE a REAPER write and mint nothing, so a site predicated on writes names points
|
||||||
|
REAPER then declines to append — history unchanged, Undo button relabelled. What a
|
||||||
|
membership edit's point holds is its `view_state` write, which is why that write is
|
||||||
|
INSIDE the block and the predicate is the model diff
|
||||||
|
(`shell/actions/design_view_actions`' `runMembershipEdit`).
|
||||||
- **ONE mode switch is ONE undo point — the background FX parking included.** The
|
- **ONE mode switch is ONE undo point — the background FX parking included.** The
|
||||||
driven flags, the lane state, AND the per-FX offline/online the reclaimer writes
|
driven flags, the lane state, AND the per-FX offline/online the reclaimer writes
|
||||||
minutes-of-plugin-loads later all roll back in a single Ctrl-Z, because they share
|
minutes-of-plugin-loads later all roll back in a single Ctrl-Z, because they share
|
||||||
@@ -222,8 +226,9 @@ decide membership or mode rules.
|
|||||||
`render_in_place`'s inner reapply. The open half is the criterion above (a reapply
|
`render_in_place`'s inner reapply. The open half is the criterion above (a reapply
|
||||||
re-asserts derived state and is nobody's act); the close half is the nesting rule
|
re-asserts derived state and is nobody's act); the close half is the nesting rule
|
||||||
above (a reapply runs inside its driver's block, so a supersede-close from there
|
above (a reapply runs inside its driver's block, so a supersede-close from there
|
||||||
would land at depth 2->1 and hijack it). It reports what it wrote instead, and the
|
would land at depth 2->1 and hijack it). It reports only whether it `applied`; a
|
||||||
driver holding the block decides — `ModeApplyResult::wroteProjectState`.
|
driver holding a block decides its point from its OWN model diff, never from what the
|
||||||
|
reapply wrote, and `ModeApplyResult` no longer offers that answer to be misread.
|
||||||
- **A rapid A→B→A flip moves zero plugins.** The reclaimer only starts a park's
|
- **A rapid A→B→A flip moves zero plugins.** The reclaimer only starts a park's
|
||||||
OFFLINE work once a track has read continuously parked for `kParkHoldMs`
|
OFFLINE work once a track has read continuously parked for `kParkHoldMs`
|
||||||
(3000ms). A flip inside that window flips the flags twice but the debounce
|
(3000ms). A flip inside that window flips the flags twice but the debounce
|
||||||
@@ -289,7 +294,7 @@ applies the resulting lane state to live tracks.
|
|||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
- `view` — Design View shell: runs the pure planner over the live folder tree and iterates its park/restore plans, applies managed-lane and parent-visibility writes, and on a REAL SWITCH ends any prior span, raises the reclaimer's wakeup, and opens the ONE undo span all of that rides (`fx_reclaim` is what closes it; a reapply opens none and closes none). Reports what it wrote (`ModeApplyResult`) so a caller holding its own block can discard an empty point. Owns the one discriminator (`target != active`) that separates a real switch from a reapply, and with it both the playback gate (`transportBlocksModeSwitch`) and the solo cache/clear/restore seams. **Never touches master or `B_MUTE`.** FX are NOT written here — see `fx_reclaim`.
|
- `view` — Design View shell: runs the pure planner over the live folder tree and iterates its park/restore plans, applies managed-lane and parent-visibility writes, and on a REAL SWITCH ends any prior span, raises the reclaimer's wakeup, and opens the ONE undo span all of that rides (`fx_reclaim` is what closes it; a reapply opens none and closes none). Reports whether it applied and nothing else (`ModeApplyResult`) — what it wrote is deliberately not an answer it gives, because none of it mints. Owns the one discriminator (`target != active`) that separates a real switch from a reapply, and with it both the playback gate (`transportBlocksModeSwitch`) and the solo cache/clear/restore seams. **Never touches master or `B_MUTE`.** FX are NOT written here — see `fx_reclaim`.
|
||||||
- `view_park` — the park surface, FLAG DOMAIN ONLY. `parkTrack` captures the pre-park snapshot (flags AND the FX chain's identity-keyed offline state) when the model holds none, drives the four flags, and EXPOSEs a track that reads park-shaped with no record behind it rather than adopting it; `restoreTrack` writes the flags back from the snapshot and spends its flag half (`releaseSnapshotFlags`). Also `trackFlagParm` (the one `Flag` → REAPER-parameter mapping), `writeIfChanged` (the read-before-write every driven parameter goes through), `fxGuidString` (the one FX-identity read snapshot/reclaim both address FX through), `kViewUndoMask`, and `reportExposedTracks`/`describeExposedTracks` (named tracks, once per changed set per project).
|
- `view_park` — the park surface, FLAG DOMAIN ONLY. `parkTrack` captures the pre-park snapshot (flags AND the FX chain's identity-keyed offline state) when the model holds none, drives the four flags, and EXPOSEs a track that reads park-shaped with no record behind it rather than adopting it; `restoreTrack` writes the flags back from the snapshot and spends its flag half (`releaseSnapshotFlags`). Also `trackFlagParm` (the one `Flag` → REAPER-parameter mapping), `writeIfChanged` (the read-before-write every driven parameter goes through), `fxGuidString` (the one FX-identity read snapshot/reclaim both address FX through), `kViewUndoMask`, and `reportExposedTracks`/`describeExposedTracks` (named tracks, once per changed set per project).
|
||||||
- `view_lanes` — managed fixed lanes: `mintManagedLanes` splits multi-mode content onto per-mode lanes, `applyLaneOps` drives `C_LANEPLAYS` for the active mode, `reconcileManagedLanes` recovers ownership from durable `P_LANENAME` on project open. Manual lanes are never minted, driven, renamed or reassigned.
|
- `view_lanes` — managed fixed lanes: `mintManagedLanes` splits multi-mode content onto per-mode lanes, `applyLaneOps` drives `C_LANEPLAYS` for the active mode, `reconcileManagedLanes` recovers ownership from durable `P_LANENAME` on project open. Manual lanes are never minted, driven, renamed or reassigned.
|
||||||
- `view_reconcile` — the repair pass for a project state REAPER reloaded under us: `noteAppliedState` records what an apply left driven; `reconcileAppliedFlags` diffs that fingerprint against the reloaded model and hands back any track the reload no longer records but still reads driven — the fix for `UNDO_STATE_TRACKCFG` not carrying `B_SHOWINTCP`/`B_SHOWINMIXER` (DAW-confirmed). It opens no undo block: it runs in the wake of a Ctrl-Z, and its writes are flags only.
|
- `view_reconcile` — the repair pass for a project state REAPER reloaded under us: `noteAppliedState` records what an apply left driven; `reconcileAppliedFlags` diffs that fingerprint against the reloaded model and hands back any track the reload no longer records but still reads driven — the fix for `UNDO_STATE_TRACKCFG` not carrying `B_SHOWINTCP`/`B_SHOWINMIXER` (DAW-confirmed). It opens no undo block: it runs in the wake of a Ctrl-Z, and its writes are flags only.
|
||||||
@@ -339,10 +344,11 @@ applies the resulting lane state to live tracks.
|
|||||||
unsaved project — land inside the block after all. That is a consequence of the
|
unsaved project — land inside the block after all. That is a consequence of the
|
||||||
span, not a decision made here; it does not resolve the redo hypothesis below,
|
span, not a decision made here; it does not resolve the redo hypothesis below,
|
||||||
which turns on capture-time semantics the SDK does not document. `[verify — DAW]`
|
which turns on capture-time semantics the SDK does not document. `[verify — DAW]`
|
||||||
that a Save-As dialog opening under an open undo block is harmless. On the reapply
|
that a Save-As dialog opening under an open undo block is harmless. On the MEMBERSHIP
|
||||||
path the write stays OUTSIDE every block, as it always did: the reapply opens none,
|
path the write is now deliberately inside the action's own `UndoBlock` — it is the
|
||||||
and a membership action's own `ActionUndoBlock` closes before `persistViewState` for
|
only thing there a point can hold — and the persist is split so its modal Save-As half
|
||||||
exactly this modal-Save-As reason. The UNDO direction still comes
|
runs BEFORE that block opens (`shell/actions/design_view_actions`, which owns the
|
||||||
|
reasoning). The UNDO direction still comes
|
||||||
out right — the point closed over the ext state as it stood BEFORE the switch,
|
out right — the point closed over the ext state as it stood BEFORE the switch,
|
||||||
which is what a Ctrl-Z should restore — but by position, not by design. The
|
which is what a Ctrl-Z should restore — but by position, not by design. The
|
||||||
REDO direction is what this leaves open, and the cost is worse than the
|
REDO direction is what this leaves open, and the cost is worse than the
|
||||||
@@ -363,9 +369,13 @@ applies the resulting lane state to live tracks.
|
|||||||
semantics for the state mask; (2) that a redo fires `BeginLoadProjectState`
|
semantics for the state mask; (2) that a redo fires `BeginLoadProjectState`
|
||||||
with `isUndo == true` so the session really does reload the stale ext state
|
with `isUndo == true` so the session really does reload the stale ext state
|
||||||
(asserted in `main.cpp`, never observed). If (2) is false the in-memory model
|
(asserted in `main.cpp`, never observed). If (2) is false the in-memory model
|
||||||
keeps its snapshots and the redo is consistent. `[verify — DAW]`. Do not "fix"
|
keeps its snapshots and the redo is consistent. `[verify — DAW]`. Link (1) is the
|
||||||
it by moving `persistViewState` inside the block — it can open a modal Save-As;
|
same capture-time reading the membership path's fix now rests on, and the nearest
|
||||||
whether to split the persist is a separate decision.
|
thing to evidence for it is `reaper_plugin.h:1198-1201`: REAPER serializes an
|
||||||
|
extension's state for a point through `SaveExtensionConfig` with `isUndo` set — i.e.
|
||||||
|
at point creation, not later. Settling it by moving the SWITCH path's
|
||||||
|
`persistViewState` is still the wrong move: that one runs with the span open, so its
|
||||||
|
modal would stand inside a block nothing closes until the drain converges.
|
||||||
- A DIFFERENT strand entirely, which the EXPOSE report does NOT cover: an
|
- A DIFFERENT strand entirely, which the EXPOSE report does NOT cover: an
|
||||||
absent or malformed `view_state` (`loadViewModel`, `ext_state_io.cpp`) is not
|
absent or malformed `view_state` (`loadViewModel`, `ext_state_io.cpp`) is not
|
||||||
a parked-chain reading at all — it falls back to a DEFAULT model (active mode
|
a parked-chain reading at all — it falls back to a DEFAULT model (active mode
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ ModeApplyResult applyModeImpl(ViewModeModel& model, const std::string& targetMod
|
|||||||
(targetMode ? targetMode->displayName : targetModeId) + " view";
|
(targetMode ? targetMode->displayName : targetModeId) + " view";
|
||||||
|
|
||||||
bool laneModeChanged = false;
|
bool laneModeChanged = false;
|
||||||
bool wrote = false; // REAPER writes only — see ModeApplyResult
|
|
||||||
std::vector<std::string> exposedNames; // named while handles are fresh, reported after the writes
|
std::vector<std::string> exposedNames; // named while handles are fresh, reported after the writes
|
||||||
|
|
||||||
// ONLY A REAL SWITCH, both halves — end a prior switch's still-open span, then open
|
// ONLY A REAL SWITCH, both halves — end a prior switch's still-open span, then open
|
||||||
@@ -155,7 +154,6 @@ ModeApplyResult applyModeImpl(ViewModeModel& model, const std::string& targetMod
|
|||||||
MediaTrack* tr = resolve(trackByGuid, guid);
|
MediaTrack* tr = resolve(trackByGuid, guid);
|
||||||
if (!tr) continue; // stale/deleted GUID — prune
|
if (!tr) continue; // stale/deleted GUID — prune
|
||||||
const TrackParkResult r = parkTrack(model, guid, tr, tp.flags);
|
const TrackParkResult r = parkTrack(model, guid, tr, tp.flags);
|
||||||
if (r.wrote) wrote = true;
|
|
||||||
// Named here, not after the block: TrackList_AdjustWindows below can
|
// Named here, not after the block: TrackList_AdjustWindows below can
|
||||||
// invalidate the handle.
|
// invalidate the handle.
|
||||||
if (r.exposed) exposedNames.push_back(trackDisplayName(tr));
|
if (r.exposed) exposedNames.push_back(trackDisplayName(tr));
|
||||||
@@ -166,14 +164,13 @@ ModeApplyResult applyModeImpl(ViewModeModel& model, const std::string& targetMod
|
|||||||
const std::string& guid = tp.flags.front().guid;
|
const std::string& guid = tp.flags.front().guid;
|
||||||
MediaTrack* tr = resolve(trackByGuid, guid);
|
MediaTrack* tr = resolve(trackByGuid, guid);
|
||||||
if (!tr) continue; // stale/deleted GUID — prune
|
if (!tr) continue; // stale/deleted GUID — prune
|
||||||
if (restoreTrack(model, guid, tr, tp.flags)) wrote = true;
|
restoreTrack(model, guid, tr, tp.flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MANAGED LANES: the active mode's lane plays+shows, every other managed lane
|
// 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.
|
// is silenced+hidden. Empty on a D1-only project — byte-identical there.
|
||||||
const LaneApplyResult lanes = applyLaneOps(trackByGuid, plan.lanes);
|
const LaneApplyResult lanes = applyLaneOps(trackByGuid, plan.lanes);
|
||||||
laneModeChanged = lanes.freeMode;
|
laneModeChanged = lanes.freeMode;
|
||||||
if (lanes.wrote) wrote = true;
|
|
||||||
|
|
||||||
// PARENT VISIBILITY (never parked): recomputed every toggle, never recorded.
|
// PARENT VISIBILITY (never parked): recomputed every toggle, never recorded.
|
||||||
// Only the two visibility flags — never mainSend/FX on a parent.
|
// Only the two visibility flags — never mainSend/FX on a parent.
|
||||||
@@ -183,8 +180,8 @@ ModeApplyResult applyModeImpl(ViewModeModel& model, const std::string& targetMod
|
|||||||
MediaTrack* tr = resolve(trackByGuid, node.guid);
|
MediaTrack* tr = resolve(trackByGuid, node.guid);
|
||||||
if (!tr) continue; // stale GUID — prune
|
if (!tr) continue; // stale GUID — prune
|
||||||
const double show = visible.count(node.guid) ? 1.0 : 0.0;
|
const double show = visible.count(node.guid) ? 1.0 : 0.0;
|
||||||
if (writeIfChanged(tr, "B_SHOWINTCP", show)) wrote = true;
|
writeIfChanged(tr, "B_SHOWINTCP", show);
|
||||||
if (writeIfChanged(tr, "B_SHOWINMIXER", show)) wrote = true;
|
writeIfChanged(tr, "B_SHOWINMIXER", show);
|
||||||
}
|
}
|
||||||
|
|
||||||
model.setActiveMode(targetModeId);
|
model.setActiveMode(targetModeId);
|
||||||
@@ -225,7 +222,7 @@ ModeApplyResult applyModeImpl(ViewModeModel& model, const std::string& targetMod
|
|||||||
// an undo rolling the model back underneath it. In memory, never on disk.
|
// an undo rolling the model back underneath it. In memory, never on disk.
|
||||||
noteAppliedState(project, model);
|
noteAppliedState(project, model);
|
||||||
|
|
||||||
return ModeApplyResult{true, wrote};
|
return ModeApplyResult{true};
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -29,17 +29,16 @@ bool modeApplyInProgress();
|
|||||||
|
|
||||||
// What one applyMode did.
|
// What one applyMode did.
|
||||||
//
|
//
|
||||||
// `wroteProjectState` counts REAPER writes ONLY — a driven flag, a lane parameter,
|
// Deliberately does NOT report whether it wrote project state. It once did, and a
|
||||||
// a parent's visibility. Model moves (a snapshot stored, a record's flag half spent,
|
// caller holding its own block used that as its mint-or-discard predicate — which is
|
||||||
// the active mode set) are deliberately excluded: they reach disk through
|
// wrong twice over: everything a reapply writes is the four driven flags, which mint
|
||||||
// persistViewState, which every caller runs OUTSIDE its undo block, so no point can
|
// no undo state at all, so the answer was true for writes REAPER will not hold and the
|
||||||
// carry them and counting them would only mint an empty one. It is the answer a
|
// resulting label re-titled the Undo button over an entry that was never appended. What
|
||||||
// caller holding its own block needs for the discard decision (design_view_actions'
|
// a membership edit's point actually holds is its `view_state` write; the caller's own
|
||||||
// ActionUndoBlock); the solo pair cannot affect it, since only a real switch drives
|
// model diff is what says whether there was one (design_view_actions'
|
||||||
// solo and a real switch mints regardless.
|
// runMembershipEdit). Do not reintroduce a wrote-project-state return here.
|
||||||
struct ModeApplyResult {
|
struct ModeApplyResult {
|
||||||
bool applied = false;
|
bool applied = false;
|
||||||
bool wroteProjectState = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Captures each about-to-park track's prior flag values into `model`, caches/clears
|
// Captures each about-to-park track's prior flag values into `model`, caches/clears
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ class ReaProject;
|
|||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
// `freeMode` alone decides whether UpdateTimeline is owed (SDK: required after an
|
// `freeMode` alone decides whether UpdateTimeline is owed (SDK: required after an
|
||||||
// I_FREEMODE change); `wrote` is a REAPER write, the reading ModeApplyResult
|
// I_FREEMODE change); `wrote` is a REAPER write and nothing more — not an undo-point
|
||||||
// (view.h) carries through to the caller's undo-point decision.
|
// predicate (these writes mint no undo state), and read by nothing today.
|
||||||
struct LaneApplyResult {
|
struct LaneApplyResult {
|
||||||
bool freeMode = false;
|
bool freeMode = false;
|
||||||
bool wrote = false;
|
bool wrote = false;
|
||||||
|
|||||||
@@ -93,13 +93,11 @@ extern const int kViewUndoMask;
|
|||||||
// Returns whether it wrote.
|
// Returns whether it wrote.
|
||||||
bool writeIfChanged(MediaTrack* tr, const char* parm, double value);
|
bool writeIfChanged(MediaTrack* tr, const char* parm, double value);
|
||||||
|
|
||||||
// What one parkTrack call did. `wrote` is a REAPER write — a driven flag moved —
|
// What one parkTrack call did. `wrote` is a REAPER write — a driven flag moved — and
|
||||||
// and nothing else: storing the snapshot is a MODEL move, and the model reaches disk
|
// nothing else; storing the snapshot is a MODEL move and does not count. It is NOT an
|
||||||
// outside every undo block, so counting it would mint a point carrying none of it
|
// undo-point predicate and never was a sound one: the driven flags mint no undo state,
|
||||||
// (ModeApplyResult, view.h). The two readings part company on exactly one path — a
|
// so no point can hold what this reports (view.h's ModeApplyResult, which used to carry
|
||||||
// RE-PARK whose restore had already handed the flags back at park-shaped values, which
|
// it up, no longer does). Nothing reads it today.
|
||||||
// stores a fresh snapshot and writes no flag — and the REAPER reading is the one that
|
|
||||||
// wants: nothing in the project moved, so nothing is there for a point to hold.
|
|
||||||
// `exposed` is true iff this call handed the track back visible instead of parking or
|
// `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
|
// adopting it; the caller (view.cpp) collects exposed tracks across the whole apply for
|
||||||
// the one batched console report.
|
// the one batched console report.
|
||||||
|
|||||||
Reference in New Issue
Block a user