fix(banks): enforce unique bank display names in-model + B3 review minors

This commit is contained in:
2026-07-25 01:29:47 -04:00
parent cbfc13c4e0
commit ccf65415c9
6 changed files with 169 additions and 22 deletions
+41 -13
View File
@@ -331,11 +331,14 @@ gaccel_register_t g_accelBankCopySel{};
gaccel_register_t g_accelBankPoolFull{};
gaccel_register_t g_accelBankBanksFull{};
// Persists the book after a bank mutation. Mirrors the capture path (main.cpp
// RunCapture): a bank change is held in-session and written to the active project's
// ext state so it travels with the .rpp. No Save-As prompt here — saveToActiveProject
// no-ops on an unsaved project (the change stays valid for the session and persists
// on the user's next save), matching how capture persists.
// Persists the book after a bank mutation. Mirrors the CAPTURE path (main.cpp
// RunCapture), NOT the Design-View path: a bank change is held in-session and written
// to the active project's ext state so it travels with the .rpp. Deliberately no
// Save-As prompt — saveToActiveProject no-ops on an unsaved project (the change stays
// valid for the session and persists on the user's next save), exactly as capture
// persists. This is an intentional divergence from persistViewState (above), which
// DOES prompt Save-As on an unsaved project; do not "align" the two — a bank mutation
// follows capture's quiet-persist idiom, a Design-View mutation follows the prompt idiom.
void persistBook() { g_session->saveToActiveProject(); }
// Prompts the user for a single line of text via REAPER's stock input dialog.
@@ -343,12 +346,21 @@ void persistBook() { g_session->saveToActiveProject(); }
// cancel (SDK ~3808). `initial` pre-fills the field. Returns false (leaving `out`
// untouched) on cancel or an empty entry. Self-contained bindable-action name entry;
// B4's panel affordances supersede this with in-panel editing.
//
// COMMA GUARD: GetUserInputs splits the returned values on a separator that defaults
// to ',', so a bank name containing a comma would be truncated at the comma. We
// override the return separator to \x1f (ASCII unit separator, un-typeable in the
// dialog) via the documented `separator=X` extra caption field (SDK ~3806), so any
// printable name — commas included — round-trips whole. The captions_csv itself stays
// comma-joined: the single field caption, then the `separator=` directive as a
// trailing pseudo-caption (the directive redefines only the RETURN separator).
bool promptText(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());
if (!GetUserInputs(title, 1, caption, buf.data(), static_cast<int>(buf.size())))
const std::string captions = std::string(caption) + ",separator=\x1f";
if (!GetUserInputs(title, 1, captions.c_str(), buf.data(), static_cast<int>(buf.size())))
return false; // user cancelled
std::string s(buf.data());
if (s.empty()) return false; // an empty name is not a valid bank name
@@ -368,9 +380,11 @@ std::string mintBankId() {
}
// Resolves a user-typed bank reference (a display name) to a bank id, scanning the
// book's banks in ordinal order. Case-sensitive exact match on displayName; "Pool"
// resolves the pool. Returns "" when no bank carries that name. Kept in the action
// layer (not the model) — it is UI name-resolution, not a model rule.
// book's banks in ordinal order. Exact match on displayName; "Pool" resolves the pool.
// Returns "" when no bank carries that name. Kept in the action layer (not the model)
// — it is UI name-resolution, not a model rule. First-match is unambiguous BY
// CONSTRUCTION: the model enforces unique display names (trimmed + case-insensitive),
// so at most one bank can carry a given name — no duplicate can shadow another here.
std::string bankIdByDisplayName(const std::string& name) {
for (const Bank& b : g_session->book().banks())
if (b.displayName == name) return b.id;
@@ -381,14 +395,18 @@ std::string bankIdByDisplayName(const std::string& name) {
// Create a named bank: prompt for a display name, mint a stable GUID id, create it in
// the model, persist. The new bank is NOT auto-activated (create and activate are
// distinct acts — mirrors capture/placement separation). A duplicate-name is allowed
// (display names are not unique in the model); the fresh GUID keeps the id unique.
// distinct acts — mirrors capture/placement separation). The model rejects a display
// name that duplicates an existing bank's (trimmed + case-insensitive, incl. "Pool");
// the 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;
const std::string id = mintBankId();
if (!g_session->book().createBank(id, name)) {
ShowConsoleMsg("ReaSampler: could not create bank (id collision — try again).\n");
ShowConsoleMsg(
("ReaSampler: could not create bank \"" + name +
"\" (a bank with that name already exists).\n")
.c_str());
return;
}
persistBook();
@@ -411,7 +429,10 @@ void doBankRename() {
std::string newName;
if (!promptText("ReaSampler: rename bank", "New name:", which, newName)) return;
if (!g_session->book().renameBank(id, newName)) {
ShowConsoleMsg("ReaSampler: cannot rename that bank (the pool is un-renamable).\n");
// renameBank rejects the pool (un-renamable) or a name already used by another
// bank (unique display names, trimmed + case-insensitive).
ShowConsoleMsg("ReaSampler: cannot rename that bank (the pool is un-renamable, "
"or another bank already uses that name).\n");
return;
}
persistBook();
@@ -432,6 +453,13 @@ void doBankDelete() {
ShowConsoleMsg(("ReaSampler: no bank named \"" + which + "\".\n").c_str());
return;
}
// Pool early-out: the pool is un-deletable (the model rejects it). Catch it here,
// BEFORE the non-empty confirm, so typing "Pool" never shows a misleading
// "delete anyway?" prompt for an operation the model will refuse regardless.
if (id == kPoolBankId) {
ShowConsoleMsg("ReaSampler: the pool cannot be deleted.\n");
return;
}
// Read member count BEFORE deleting (the Bank* is invalidated by deleteBank; we do
// not cache it — resolve size to an int up front).
const Bank* b = g_session->book().bank(id);