diff --git a/src/shell/actions/bank_actions.cpp b/src/shell/actions/bank_actions.cpp index 67ddf94..0defb26 100644 --- a/src/shell/actions/bank_actions.cpp +++ b/src/shell/actions/bank_actions.cpp @@ -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()); diff --git a/src/shell/panel/panel_bank_ops.cpp b/src/shell/panel/panel_bank_ops.cpp index d3d8b3b..497c326 100644 --- a/src/shell/panel/panel_bank_ops.cpp +++ b/src/shell/panel/panel_bank_ops.cpp @@ -95,7 +95,7 @@ std::vector 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 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() -> diff --git a/src/shell/panel/panel_bank_ops.h b/src/shell/panel/panel_bank_ops.h index afe98b6..2f1c5ac 100644 --- a/src/shell/panel/panel_bank_ops.h +++ b/src/shell/panel/panel_bank_ops.h @@ -67,8 +67,8 @@ bool bankOpRemove(const std::vector& 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 /