feat(persist): reload book+view on undo/redo via projectconfig hook (R-B)

BeginLoadProjectState(isUndo) requests a deferred reload the timer drains
next tick, once REAPER has restored ext-state. Hook owns undo/redo; identity
poll still owns open/tab-switch/save-as. Fixes copy-collapse over-count
(verb-aware guard) and dangling undo point on unsaved-project bank ops.
This commit is contained in:
2026-07-26 06:59:26 -04:00
parent 6d372794f5
commit a07b28fd65
4 changed files with 158 additions and 18 deletions
+26 -9
View File
@@ -453,7 +453,9 @@ gaccel_register_t g_accelBankBanksFull{};
// persists. This is an intentional divergence from persistViewState (above), which
// DOES prompt Save-As on an unsaved project; do not "align" the two — a bank mutation
// follows capture's quiet-persist idiom, a Design-View mutation follows the prompt idiom.
void persistBook() { g_session->saveToActiveProject(); }
// Returns whether a persist actually happened (false on an unsaved/no-active project),
// so persistBankOp can skip its undo block when nothing was written.
bool persistBook() { return g_session->saveToActiveProject(); }
// Persists a completed bank index verb (create/rename/reorder/delete/evacuate/
// move/copy) as a SINGLE batched REAPER undo point (R-B) — one bank op = one Ctrl-Z.
@@ -469,11 +471,22 @@ void persistBook() { g_session->saveToActiveProject(); }
//
// NO-OP GUARDRAIL: callers invoke this ONLY after the model mutation succeeded — a
// rejected op (duplicate name, un-deletable pool, etc.) returns before reaching here,
// so no dangling/empty undo point is ever opened.
// so no dangling/empty undo point is ever opened for a rejected op.
//
// UNSAVED-PROJECT GUARDRAIL: on an unsaved / no-active project persistBook() no-ops
// (nothing is written to ext state). We must still CLOSE the block we opened, but with
// an EMPTY label and a zero flag so REAPER DISCARDS the point instead of recording a
// no-effect undo entry — mirroring view.cpp's empty-plan close. The in-session model
// change stands and persists on the user's next save; it just earns no undo point until
// there is a project to persist into (undo of an unsaved bank op has nothing to roll
// back to anyway). The Begin/End must still be balanced, hence the close-either-way.
void persistBankOp(const char* label) {
Undo_BeginBlock2(nullptr);
persistBook();
Undo_EndBlock2(nullptr, label, UNDO_STATE_MISCCFG);
const bool persisted = persistBook();
if (persisted)
Undo_EndBlock2(nullptr, label, UNDO_STATE_MISCCFG);
else
Undo_EndBlock2(nullptr, "", 0); // no ext-state write -> discard the empty point
}
// Prompts the user for a single line of text via REAPER's stock input dialog.
@@ -713,11 +726,15 @@ void doBankTransferSelected(bool copy) {
case TransferResult::RejectedSameBank: break;
}
}
// No-op guardrail: if nothing actually changed the index (every selected sample was
// absent, or all were pre-checked rejects), don't open an undo point. `collapsed`
// counts a hash-collapse — that DID mutate the index (source entry removed on a
// move, or dest already held the hash), so it belongs inside the undo point.
if (ok > 0 || collapsed > 0) {
// No-op guardrail — VERB-AWARE (a collapse means different things per verb):
// * MOVE collapse: the source entry WAS removed (bank_book moveSample removes
// unconditionally before the dest add collapses on hash), so the index DID
// mutate — it counts toward opening an undo point.
// * COPY collapse: the source is left intact AND the dest already held the hash,
// so NOTHING changed — a true index no-op. It must NOT open an undo point.
// Hence: copy counts only real gains (ok); move counts gains OR collapses.
const bool mutated = copy ? (ok > 0) : (ok > 0 || collapsed > 0);
if (mutated) {
const std::string label =
std::string("ReaSampler: ") + verb + " sample(s)";
persistBankOp(label.c_str());