fix: guard persistBankOp/persistBook against a null session; rename promptText to promptBankName

This commit is contained in:
2026-07-29 13:06:12 -04:00
parent 430e117620
commit 4f587258b2
3 changed files with 34 additions and 17 deletions
+8 -8
View File
@@ -1,7 +1,7 @@
// bank_actions.cpp — the multi-bank bindable action family (Phase B3; Q-W4 split of
// actions.cpp). See bank_actions.h.
//
// Q-W4 dedupe: each mutating handler is a THIN UX SKIN — text prompts (promptText),
// Q-W4 dedupe: each mutating handler is a THIN UX SKIN — text prompts (promptBankName),
// name resolution, and console feedback — over the promptless bankOp* inner verbs
// homed in panel_bank_ops (model op + persistBankOp, one bank op = one Ctrl-Z). The
// book's rules (pool privileges, collapse-by-hash, active-fallback-to-pool) all live
@@ -28,7 +28,7 @@
#include "core/model/bank_book.h" // BankBook, nextBankId, kPoolBankId (B1)
#include "persist.h" // ReaSamplerSession (owns book())
#include "shell/panel/panel_bank_ops.h" // bankOp* inner verbs + promptText + selection seam
#include "shell/panel/panel_bank_ops.h" // bankOp* inner verbs + promptBankName + selection seam
#include "shell/panel/panel_layout.h" // full-height toggles (B3)
#define REAPERAPI_MINIMAL
@@ -115,7 +115,7 @@ std::string bankIdByDisplayName(const std::string& name) {
// create then fails and the user is told the name is taken.
void doBankCreate() {
std::string name;
if (!promptText("ReaSampler: create bank", "Bank name:", "", name)) return;
if (!promptBankName("ReaSampler: create bank", "Bank name:", "", name)) return;
if (bankOpCreate(name).empty()) {
ShowConsoleMsg(
("ReaSampler: could not create bank \"" + name +
@@ -129,7 +129,7 @@ void doBankCreate() {
// self-contained; the panel renames in place on a tab.
void doBankRename() {
std::string which;
if (!promptText("ReaSampler: rename bank", "Bank to rename (current name):", "",
if (!promptBankName("ReaSampler: rename bank", "Bank to rename (current name):", "",
which))
return;
const std::string id = bankIdByDisplayName(which);
@@ -138,7 +138,7 @@ void doBankRename() {
return;
}
std::string newName;
if (!promptText("ReaSampler: rename bank", "New name:", which, newName)) return;
if (!promptBankName("ReaSampler: rename bank", "New name:", which, newName)) return;
if (!bankOpRename(id, newName)) {
// The verb rejects the pool (un-renamable) or a name already used by another
// bank (unique display names, trimmed + case-insensitive).
@@ -154,7 +154,7 @@ void doBankRename() {
// panel confirm (naming evacuate inline, with a one-click evacuate) lives in the panel.
void doBankDelete() {
std::string which;
if (!promptText("ReaSampler: delete bank", "Bank to delete:", "", which)) return;
if (!promptBankName("ReaSampler: delete bank", "Bank to delete:", "", which)) return;
const std::string id = bankIdByDisplayName(which);
if (id.empty()) {
ShowConsoleMsg(("ReaSampler: no bank named \"" + which + "\".\n").c_str());
@@ -194,7 +194,7 @@ void doBankDelete() {
// intended "keep the samples" companion to delete.
void doBankEvacuate() {
std::string which;
if (!promptText("ReaSampler: evacuate bank", "Bank to evacuate to the pool:", "",
if (!promptBankName("ReaSampler: evacuate bank", "Bank to evacuate to the pool:", "",
which))
return;
const std::string id = bankIdByDisplayName(which);
@@ -245,7 +245,7 @@ void doBankTransferSelected(bool copy) {
const char* verb = copy ? "copy" : "move";
const std::string title = std::string("ReaSampler: ") + verb + " selected samples";
std::string destName;
if (!promptText(title.c_str(), "Destination bank:", "", destName)) return;
if (!promptBankName(title.c_str(), "Destination bank:", "", destName)) return;
const std::string destId = bankIdByDisplayName(destName);
if (destId.empty()) {
ShowConsoleMsg(("ReaSampler: no bank named \"" + destName + "\".\n").c_str());
+18 -7
View File
@@ -95,7 +95,7 @@ std::vector<const Bank*> namedBanks() {
void doCreateBank() {
if (!book()) return;
std::string name;
if (!promptText("ReaSampler: create bank", "Bank name:", "", name)) return;
if (!promptBankName("ReaSampler: create bank", "Bank name:", "", name)) return;
const std::string id = bankOpCreate(name);
if (id.empty()) {
ShowMessageBox("A bank with that name already exists.",
@@ -115,7 +115,7 @@ void doRenameBank(const std::string& bankId) {
if (!bk || bk->isPool()) return;
const std::string current = bk->displayName; // copy before any mutation
std::string newName;
if (!promptText("ReaSampler: rename bank", "New name:", current, newName)) return;
if (!promptBankName("ReaSampler: rename bank", "New name:", current, newName)) return;
if (!bankOpRename(bankId, newName)) {
ShowMessageBox("Another bank already uses that name.",
"ReaSampler: rename bank", 0);
@@ -417,9 +417,13 @@ namespace {
// (the change stays valid for the session and persists on the user's next save).
// Deliberately NO Save-As prompt; do not "align" with persistViewState's prompt
// idiom. Returns whether a persist actually happened, so persistBankOp can discard
// its undo block when nothing was written. Session pointer is live for the whole
// extension lifetime (bankPanelInit at load, before any action registers).
bool persistBook() { return panel::g_panel.session->saveToActiveProject(); }
// its undo block when nothing was written. Guards a null session pointer (false,
// no-op) — see persistBankOp's guard below for why this is defensive rather than
// dead code.
bool persistBook() {
if (!panel::g_panel.session) return false; // no live session: nothing to persist
return panel::g_panel.session->saveToActiveProject();
}
// Mints a fresh, genuine REAPER GUID string as a stable bank id (the B2/model
// design: ids are caller-supplied and stable; the model stays pure and mints none).
@@ -438,8 +442,8 @@ std::string mintBankId() {
// COMMA GUARD: GetUserInputs splits returned values on a separator defaulting to ',',
// so the return separator is overridden to \x1f (un-typeable) via the documented
// `separator=X` trailing pseudo-caption (SDK ~3806) — any printable name round-trips.
bool promptText(const char* title, const char* caption, const std::string& initial,
std::string& out) {
bool promptBankName(const char* title, const char* caption, const std::string& initial,
std::string& out) {
std::vector<char> buf(512, '\0');
// Pre-fill: GetUserInputs seeds the field from the retvals buffer's initial value.
std::snprintf(buf.data(), buf.size(), "%s", initial.c_str());
@@ -472,7 +476,14 @@ bool promptText(const char* title, const char* caption, const std::string& initi
// 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.
//
// NULL-SESSION GUARD: this is a public API (panel_bank_ops.h) with callers outside
// this TU (e.g. panel_drag.cpp), not all of which are guaranteed to have re-checked
// the session pointer immediately beforehand. Bail out BEFORE Undo_BeginBlock2 — no
// block is opened, so there is nothing to balance and no risk of an unbalanced
// Begin/End pair.
void persistBankOp(const char* label, bool bumpGeneration) {
if (!panel::g_panel.session) return; // no live session: no-op, no undo point opened
Undo_BeginBlock2(nullptr);
// S9: bump the bank-generation counter INSIDE the block, before persistBook(), so the
// fresh generation rides the same ext-state write the persist makes (persistBook() ->
+8 -2
View File
@@ -67,8 +67,8 @@ bool bankOpRemove(const std::vector<std::string>& sampleIds,
// overridden to \x1f (un-typeable) via the documented `separator=X` pseudo-caption,
// so any printable name — commas included — round-trips whole (SDK ~3806/3808).
// One home (Q-W4) for the former actions.cpp/panel_bank_ops.cpp twins.
bool promptText(const char* title, const char* caption, const std::string& initial,
std::string& out);
bool promptBankName(const char* title, const char* caption, const std::string& initial,
std::string& out);
// Persists a completed bank-index verb as a single REAPER undo point (R-B).
// Wraps the session persist (SetProjExtState) in a Begin/End block with
@@ -78,6 +78,12 @@ bool promptText(const char* title, const char* caption, const std::string& initi
// 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.
//
// NULL-SESSION GUARD: this is a public API with callers outside panel_bank_ops.cpp
// (e.g. panel_drag.cpp). If the panel's session pointer is absent (no live session),
// this is a no-op — no undo block is opened. Today every real caller only reaches
// here via a prior session-backed check, so the guard is not yet reachable in
// practice; it exists to make the function safe to call standalone.
//
// S9 bank-generation bump: pass `bumpGeneration = true` for a verb that changes what a
// live instance would PLAY — move / copy / remove / evacuate / delete-with-members. Leave
// it false (the default) for a PURELY ORGANIZATIONAL verb — create / rename / activate /