feat(actions): batch bank index verbs into single REAPER undo points (R-B)

Wrap each bank verb's persist in Undo_BeginBlock2/EndBlock2 with UNDO_STATE_MISCCFG so one bank op is one Ctrl-Z; ext-state participates in undo per SDK. Rejected/no-op ops open no block.
This commit is contained in:
2026-07-26 05:49:38 -04:00
parent 6c6a275234
commit 6d372794f5
+36 -7
View File
@@ -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";