diff --git a/src/actions.cpp b/src/actions.cpp index 0754cd8..bf3bd07 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -455,6 +455,27 @@ gaccel_register_t g_accelBankBanksFull{}; // follows capture's quiet-persist idiom, a Design-View mutation follows the prompt idiom. void persistBook() { 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. +// +// WHY THIS WRAPS AND persistBook() DOES NOT: a bank verb mutates ONLY our project +// ext-state (SetProjExtState under "reasampler"), which REAPER's undo system captures +// iff UNDO_STATE_MISCCFG is set in the Undo_EndBlock2 flags — the SDK documents +// MISCCFG as covering "extensions!" project ext-state (reaper_plugin.h ~1544, ~1199). +// We pass exactly UNDO_STATE_MISCCFG (not -1 / UNDO_STATE_ALL as the item-move family +// does): a bank verb touches no tracks, FX, items, or envelopes, so snapshotting them +// would be both heavier and semantically wrong. persistBook() (= SetProjExtState) runs +// INSIDE the block so the post-mutation ext-state is the block's "after" image. +// +// 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. +void persistBankOp(const char* label) { + Undo_BeginBlock2(nullptr); + persistBook(); + Undo_EndBlock2(nullptr, label, UNDO_STATE_MISCCFG); +} + // Prompts the user for a single line of text via REAPER's stock input dialog. // GetUserInputs(title, num_inputs=1, captions_csv, retvals_csv, sz) -> false on // cancel (SDK ~3808). `initial` pre-fills the field. Returns false (leaving `out` @@ -523,7 +544,7 @@ void doBankCreate() { .c_str()); return; } - persistBook(); + persistBankOp("ReaSampler: create bank"); ShowConsoleMsg(("ReaSampler: created bank \"" + name + "\".\n").c_str()); } @@ -549,7 +570,7 @@ void doBankRename() { "or another bank already uses that name).\n"); return; } - persistBook(); + persistBankOp("ReaSampler: rename bank"); ShowConsoleMsg(("ReaSampler: renamed \"" + which + "\" -> \"" + newName + "\".\n") .c_str()); } @@ -593,7 +614,7 @@ void doBankDelete() { ShowConsoleMsg("ReaSampler: cannot delete that bank (the pool is un-deletable).\n"); return; } - persistBook(); + persistBankOp("ReaSampler: delete bank"); ShowConsoleMsg(("ReaSampler: deleted bank \"" + which + "\".\n").c_str()); } @@ -615,7 +636,7 @@ void doBankEvacuate() { "destination, not a source).\n"); return; } - persistBook(); + persistBankOp("ReaSampler: evacuate bank"); ShowConsoleMsg(("ReaSampler: evacuated \"" + which + "\" to the pool.\n").c_str()); } @@ -630,7 +651,7 @@ void doBankActivateNext() { const std::string target = nextBankId(ids, g_session->book().activeBankId()); if (target.empty()) return; // degenerate (no banks) — cannot happen (pool seeded) if (!g_session->book().setActiveBank(target)) return; - persistBook(); + persistBankOp("ReaSampler: activate bank"); const Bank* b = g_session->book().bank(target); ShowConsoleMsg(("ReaSampler: active bank -> \"" + (b ? b->displayName : target) + "\".\n") @@ -641,7 +662,7 @@ void doBankActivateNext() { // direct-by-id form; a general activate-bank-by-name/menu is a B4 affordance. void doBankActivatePool() { if (!g_session->book().setActiveBank(kPoolBankId)) return; - persistBook(); + persistBankOp("ReaSampler: activate bank"); ShowConsoleMsg("ReaSampler: active bank -> \"Pool\".\n"); } @@ -692,7 +713,15 @@ void doBankTransferSelected(bool copy) { case TransferResult::RejectedSameBank: break; } } - persistBook(); + // 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) { + const std::string label = + std::string("ReaSampler: ") + verb + " sample(s)"; + persistBankOp(label.c_str()); + } std::string log = std::string("ReaSampler: ") + verb + " -> \"" + destName + "\": " + std::to_string(ok) + " " + verb + "d"; if (collapsed) log += ", " + std::to_string(collapsed) + " collapsed on hash";