Merge panel-undo pass: panel gestures get R-B undo points

This commit is contained in:
2026-07-26 15:33:34 -04:00
3 changed files with 77 additions and 32 deletions
+33 -16
View File
@@ -476,22 +476,6 @@ bool persistBook() { return g_session->saveToActiveProject(); }
// rejected op (duplicate name, un-deletable pool, etc.) returns before reaching here, // rejected op (duplicate name, un-deletable pool, etc.) returns before reaching here,
// so no dangling/empty undo point is ever opened for a rejected op. // 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);
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. // 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 // GetUserInputs(title, num_inputs=1, captions_csv, retvals_csv, sz) -> false on
// cancel (SDK ~3808). `initial` pre-fills the field. Returns false (leaving `out` // cancel (SDK ~3808). `initial` pre-fills the field. Returns false (leaving `out`
@@ -827,6 +811,39 @@ void doBankRemoveSelected() {
} // namespace } // namespace
// Persists a completed bank-index verb as a SINGLE batched REAPER undo point (R-B) —
// one bank op = one Ctrl-Z. Declared in actions.h so bank_panel.cpp can call it
// without duplicating the undo logic.
//
// 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 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);
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
}
void bankRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) { void bankRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) {
g_session = session; // shared with the Design View family; same live session g_session = session; // shared with the Design View family; same live session
+9
View File
@@ -66,4 +66,13 @@ bool bankHandleCommand(int command);
// rec==nullptr (before g_session is torn down). // rec==nullptr (before g_session is torn down).
void bankUnregisterActions(reaper_plugin_info_t* rec); void bankUnregisterActions(reaper_plugin_info_t* rec);
// Persists a completed bank-index verb as a single REAPER undo point (R-B).
// Wraps persistBook() (= SetProjExtState) in a Begin/End block with UNDO_STATE_MISCCFG
// so the bank op is one Ctrl-Z. On an unsaved / no-active project persistBook() no-ops
// and the block is closed with an empty label + zero flag (REAPER discards it). Callers
// must invoke this ONLY after a successful/effective mutation — rejected ops (duplicate
// name, un-deletable pool, etc.) must return before reaching here so no empty undo
// point is ever opened for a no-op. Defined in actions.cpp alongside persistBook().
void persistBankOp(const char* label);
} // namespace reasampler } // namespace reasampler
+35 -16
View File
@@ -46,6 +46,7 @@
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include "actions.h" // persistBankOp — shared undo-block wrapper (R-B panel path)
#include "bank_book.h" #include "bank_book.h"
#include "bank_grid.h" #include "bank_grid.h"
#include "bank_model.h" #include "bank_model.h"
@@ -1179,14 +1180,11 @@ bool regionAt(int x, int y, Region& out) {
// --- Bank management ops (id-keyed; drive the B1 model + persist) -------------- // --- Bank management ops (id-keyed; drive the B1 model + persist) --------------
// //
// Each op mutates g_session.book() then persists via saveToActiveProject(). After a // Each op mutates g_session.book() then persists via persistBankOp(). After a
// STRUCTURAL mutation (create/delete/evacuate) any Bank*/BankIndex& is invalid — we // STRUCTURAL mutation (create/delete/evacuate) any Bank*/BankIndex& is invalid — we
// resolve fresh, pass ids, and let the next refreshFingerprint repaint. persistBook // resolve fresh, pass ids, and let the next refreshFingerprint repaint. On an
// no-ops on an unsaved project (matches the capture/B3 quiet-persist idiom). // unsaved project the empty-close discard in persistBankOp ensures no stale state
// survives (matches the capture/B3 quiet-persist idiom).
void persistBook() {
if (g_panel.session) g_panel.session->saveToActiveProject();
}
// REAPER's stock single-line input (comma-safe via the \x1f return separator, as B3). // REAPER's stock single-line input (comma-safe via the \x1f return separator, as B3).
bool promptText(const char* title, const char* caption, const std::string& initial, bool promptText(const char* title, const char* caption, const std::string& initial,
@@ -1224,7 +1222,7 @@ void doCreateBank() {
} }
g_panel.shownBankId = id; // show the freshly-created bank g_panel.shownBankId = id; // show the freshly-created bank
g_panel.focusedRegion = Region::Banks; g_panel.focusedRegion = Region::Banks;
persistBook(); persistBankOp("ReaSampler: create bank");
invalidatePanel(); invalidatePanel();
} }
@@ -1240,7 +1238,7 @@ void doRenameBank(const std::string& bankId) {
"ReaSampler: rename bank", 0); "ReaSampler: rename bank", 0);
return; return;
} }
persistBook(); persistBankOp("ReaSampler: rename bank");
invalidatePanel(); invalidatePanel();
} }
@@ -1273,7 +1271,7 @@ void doDeleteBank(const std::string& bankId) {
// r == 6 (Yes) falls through to a plain delete (drops members). // r == 6 (Yes) falls through to a plain delete (drops members).
} }
if (!book()->deleteBank(bankId)) return; if (!book()->deleteBank(bankId)) return;
persistBook(); persistBankOp("ReaSampler: delete bank");
// shownBankId is reconciled by the next fingerprint pass. If no named banks remain, // shownBankId is reconciled by the next fingerprint pass. If no named banks remain,
// nudge focus to the pool so the selection has a valid home. // nudge focus to the pool so the selection has a valid home.
if (namedBanks().empty()) g_panel.focusedRegion = Region::Pool; if (namedBanks().empty()) g_panel.focusedRegion = Region::Pool;
@@ -1285,30 +1283,51 @@ void doEvacuateBank(const std::string& bankId) {
const Bank* bk = book()->bank(bankId); const Bank* bk = book()->bank(bankId);
if (!bk || bk->isPool()) return; if (!bk || bk->isPool()) return;
if (!book()->evacuate(bankId)) return; if (!book()->evacuate(bankId)) return;
persistBook(); persistBankOp("ReaSampler: evacuate bank");
invalidatePanel(); invalidatePanel();
} }
void doActivateBank(const std::string& bankId) { void doActivateBank(const std::string& bankId) {
if (!book()) return; if (!book()) return;
if (!book()->setActiveBank(bankId)) return; // rejects an unknown id if (!book()->setActiveBank(bankId)) return; // rejects an unknown id
persistBook(); persistBankOp("ReaSampler: activate bank");
invalidatePanel(); invalidatePanel();
} }
// Move or copy `sampleIds` from `srcBankId` to `destBankId` (index-only). Both pass // Move or copy `sampleIds` from `srcBankId` to `destBankId` (index-only). Both pass
// ids straight to the model op (no BankIndex& cached across the loop's mutations). // ids straight to the model op (no BankIndex& cached across the loop's mutations).
//
// NO-OP GUARDRAIL — VERB-AWARE (matches the action layer's doBankTransferSelected):
// * MOVE collapse: the source entry WAS removed (bank_book removes unconditionally
// before the dest add collapses on hash), so the index DID mutate — counts.
// * COPY collapse: the source is left intact AND the dest already held the hash,
// so NOTHING changed — a true index no-op. Must NOT open an undo point.
// Hence: copy counts only real gains (Copied); move counts gains OR collapses.
void transferSamples(const std::vector<std::string>& sampleIds, void transferSamples(const std::vector<std::string>& sampleIds,
const std::string& srcBankId, const std::string& destBankId, const std::string& srcBankId, const std::string& destBankId,
bool copy) { bool copy) {
if (!book()) return; if (!book()) return;
if (sampleIds.empty() || srcBankId == destBankId) return; if (sampleIds.empty() || srcBankId == destBankId) return;
if (!book()->bank(srcBankId) || !book()->bank(destBankId)) return; if (!book()->bank(srcBankId) || !book()->bank(destBankId)) return;
int ok = 0, collapsed = 0;
for (const std::string& sid : sampleIds) { for (const std::string& sid : sampleIds) {
if (copy) book()->copySample(sid, srcBankId, destBankId); const TransferResult r =
else book()->moveSample(sid, srcBankId, destBankId); copy ? book()->copySample(sid, srcBankId, destBankId)
: book()->moveSample(sid, srcBankId, destBankId);
switch (r) {
case TransferResult::Moved:
case TransferResult::Copied: ++ok; break;
case TransferResult::Collapsed: ++collapsed; break;
case TransferResult::RejectedUnknownBank:
case TransferResult::RejectedSampleAbsent:
case TransferResult::RejectedSameBank: break;
} }
persistBook(); }
const bool mutated = copy ? (ok > 0) : (ok > 0 || collapsed > 0);
if (!mutated) return; // nothing changed — no persist, no undo point
const char* label = copy ? "ReaSampler: copy sample(s)" : "ReaSampler: move sample(s)";
persistBankOp(label);
// The selection indexed into the source; after a move those indices are stale, so // The selection indexed into the source; after a move those indices are stale, so
// clear it (the fingerprint pass will also clear, but do it now for immediacy). // clear it (the fingerprint pass will also clear, but do it now for immediacy).
g_panel.selection = Selection{}; g_panel.selection = Selection{};
@@ -1357,7 +1376,7 @@ void removeSamples(const std::vector<std::string>& sampleIds,
++removed; ++removed;
if (removed == 0) return; // nothing changed — no persist, no undo point if (removed == 0) return; // nothing changed — no persist, no undo point
persistBook(); persistBankOp("ReaSampler: remove sample(s)");
// The selection indexed into the source; after a remove those indices are stale, so // The selection indexed into the source; after a remove those indices are stale, so
// clear it (the fingerprint pass will also clear, but do it now for immediacy). // clear it (the fingerprint pass will also clear, but do it now for immediacy).
g_panel.selection = Selection{}; g_panel.selection = Selection{};