Q-W6: registration table (OCP) in main.cpp; bank verbs -> shell/bank_ops(Session&); persist.h + wav_trim + namespaces.h shims deleted; 61/61

capture.h realtime seam split to capture_realtime_shell.h; GetProjExtState grow-loop rehomed to core/wire/ext_state_read; stale persist.cpp/bank_panel.cpp comment refs fixed; CLAUDE.md persist/bank_book/actions bullets updated. Command-id suffixes, display phrases, and undo labels byte-identical.
This commit is contained in:
2026-07-29 13:40:09 -04:00
parent 4831e0e172
commit f3be4d8cce
81 changed files with 970 additions and 1085 deletions
+30 -204
View File
@@ -1,17 +1,10 @@
// panel_bank_ops.cpp — the bank-CRUD + menus seam of the docked bank panel (Q-W2
// split of bank_panel.cpp; Phase B4/B5). Since Q-W4 this TU is the ONE implementation
// home of the bank verbs (create / rename / delete / evacuate / activate / move /
// copy / remove): the promptless bankOp* inner verbs (model op + persistBankOp only)
// serve BOTH thin UX skins — the panel's menu handlers here and the bindable
// bank_actions family — plus the book/bank accessors, the popup menus that drive
// them, and the selection-id / OS-drag path resolvers.
//
// Each verb mutates the session's book() then persists via persistBankOp() (one bank
// op = one Ctrl-Z; a true index no-op opens NO undo point). It DOES mutate the bank
// BOOK — that is the whole point of B4 — but only the index/model + ext-state, never
// the arrange, never a sample file on disk (bank ops are index-only; files stay put —
// CONTEXT.md §Multi-bank). REFERENCE-INVALIDATION GUARDRAIL: after a STRUCTURAL
// mutation any Bank*/BankModel& is invalid — resolve fresh, pass ids.
// panel_bank_ops.cpp — the bank-CRUD-UX + menus seam of the docked bank panel
// (Q-W2 split of the former bank_panel god-module; Phase B4/B5). Since Q-W6 the
// promptless bank verbs live in shell/bank_ops (model op + persistBankOp, taking
// ReaSamplerSession&); this TU is the panel's THIN UX SKIN over them — the menu
// handlers (prompts / confirms / message boxes / panel-state nudges / repaint),
// the book/bank accessors, the popup menus that drive them, and the selection-id /
// OS-drag path resolvers. The bindable bank_actions family is the sibling skin.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
@@ -25,24 +18,21 @@
#include "shell/panel/panel_state.h"
#include "shell/panel/panel_bank_ops.h"
#include "persist.h" // ReaSamplerSession — the live session the ops mutate
#include "shell/bank_ops/bank_ops.h" // bankOp* promptless verbs (the Q-W6 non-UI seam)
#include "shell/persist/session.h" // ReaSamplerSession — the live session the ops mutate
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_EnumProjects
#define REAPERAPI_WANT_GetUserInputs
#define REAPERAPI_WANT_ShowMessageBox
#define REAPERAPI_WANT_Main_OnCommand
#define REAPERAPI_WANT_genGuid
#define REAPERAPI_WANT_guidToString
#define REAPERAPI_WANT_Undo_BeginBlock2
#define REAPERAPI_WANT_Undo_EndBlock2
#include "reaper_plugin_functions.h"
namespace reasampler::panel {
namespace fs = std::filesystem;
// --- Current-project directory (mirrors persist.cpp's derivation) -------------
// --- Current-project directory (mirrors the persist shell's derivation, ext_state_io.cpp)
std::string currentProjectDir() {
std::vector<char> buf(4096, '\0');
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
@@ -84,19 +74,21 @@ std::vector<const Bank*> namedBanks() {
// --- Bank management ops (id-keyed; THIN UX SKINS over the bankOp* verbs) ------
//
// Q-W4: each handler here owns only the panel's UX (prompts / confirms / message
// boxes / panel-state nudges / repaint); the model op + persist is the shared
// bankOp* inner verb (defined in the public section below). After a STRUCTURAL
// mutation (create/delete/evacuate) any Bank*/BankModel& is invalid — we resolve
// fresh, pass ids, and let the next refreshFingerprint repaint. On an unsaved
// project the empty-close discard in persistBankOp ensures no stale state
// survives (matches the capture/B3 quiet-persist idiom).
// Q-W4/Q-W6: each handler here owns only the panel's UX (prompts / confirms /
// message boxes / panel-state nudges / repaint); the model op + persist is the
// shared bankOp* inner verb (shell/bank_ops), which takes the live session by
// reference — the book() check answers the one session-liveness question per
// handler. After a STRUCTURAL mutation (create/delete/evacuate) any
// Bank*/BankModel& is invalid — we resolve fresh, pass ids, and let the next
// refreshFingerprint repaint. On an unsaved project the empty-close discard in
// persistBankOp ensures no stale state survives (matches the capture/B3
// quiet-persist idiom).
void doCreateBank() {
if (!book()) return;
std::string name;
if (!promptBankName("ReaSampler: create bank", "Bank name:", "", name)) return;
const std::string id = bankOpCreate(name);
const std::string id = bankOpCreate(*g_panel.session, name);
if (id.empty()) {
ShowMessageBox("A bank with that name already exists.",
"ReaSampler: create bank", 0);
@@ -116,7 +108,7 @@ void doRenameBank(const std::string& bankId) {
const std::string current = bk->displayName; // copy before any mutation
std::string newName;
if (!promptBankName("ReaSampler: rename bank", "New name:", current, newName)) return;
if (!bankOpRename(bankId, newName)) {
if (!bankOpRename(*g_panel.session, bankId, newName)) {
ShowMessageBox("Another bank already uses that name.",
"ReaSampler: rename bank", 0);
return;
@@ -156,7 +148,7 @@ void doDeleteBank(const std::string& bankId) {
// delete path moved/dropped members) — both change what a live instance could play. An
// empty-bank delete is purely organizational, no bump. The ORIGINAL member count decides
// (the No-path evacuated them moments ago, but the membership still changed).
if (!bankOpDelete(bankId, /*bumpGeneration=*/members > 0)) return;
if (!bankOpDelete(*g_panel.session, bankId, /*bumpGeneration=*/members > 0)) return;
// 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.
if (namedBanks().empty()) g_panel.focusedRegion = Region::Pool;
@@ -167,12 +159,13 @@ void doEvacuateBank(const std::string& bankId) {
if (!book()) return;
const Bank* bk = book()->bank(bankId);
if (!bk || bk->isPool()) return;
if (!bankOpEvacuate(bankId)) return;
if (!bankOpEvacuate(*g_panel.session, bankId)) return;
invalidatePanel();
}
void doActivateBank(const std::string& bankId) {
if (!bankOpActivate(bankId)) return; // rejects an unknown id
if (!book()) return; // no live session — nothing to activate against
if (!bankOpActivate(*g_panel.session, bankId)) return; // rejects an unknown id
invalidatePanel();
}
@@ -185,7 +178,8 @@ void doActivateBank(const std::string& bankId) {
void transferSamples(const std::vector<std::string>& sampleIds,
const std::string& srcBankId, const std::string& destBankId,
bool copy) {
if (!bankOpTransfer(sampleIds, srcBankId, destBankId, copy))
if (!book()) return; // no live session — nothing to transfer within
if (!bankOpTransfer(*g_panel.session, sampleIds, srcBankId, destBankId, copy))
return; // nothing changed — no persist, no undo point
// 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).
@@ -198,7 +192,8 @@ void transferSamples(const std::vector<std::string>& sampleIds,
// one-Ctrl-Z contract. Clears the stale selection and repaints on an actual removal.
void removeSamples(const std::vector<std::string>& sampleIds,
const std::string& srcBankId) {
if (!bankOpRemove(sampleIds, srcBankId))
if (!book()) return; // no live session — nothing to remove from
if (!bankOpRemove(*g_panel.session, sampleIds, srcBankId))
return; // nothing changed — no persist, no undo point
// 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).
@@ -410,35 +405,7 @@ void showSelectionMenu(int screenX, int screenY) {
namespace reasampler {
namespace {
// Persists the book after a bank mutation. Mirrors the CAPTURE path, NOT the
// Design-View path: quiet persist — saveToActiveProject no-ops on an unsaved project
// (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. 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).
// Distinct from a track GUID by origin only — both are canonical guidToString output.
std::string mintBankId() {
GUID g{};
genGuid(&g);
char buf[64] = {0}; // guidToString needs >=64 chars (SDK contract)
guidToString(&g, buf);
return std::string(buf);
}
} // namespace
// One home (Q-W4) for the former actions.cpp/panel_bank_ops.cpp byte-identical twins.
// One home (Q-W4) for the former actions/panel byte-identical twins.
// 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.
@@ -457,147 +424,6 @@ bool promptBankName(const char* title, const char* caption, const std::string& i
return true;
}
// Persists a completed bank-index verb 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.
//
// 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.
//
// 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() ->
// saveToActiveProject() stamps bankGeneration()). Bumped only for content-changing verbs
// (the caller decides); a pure-organizational verb passes false and leaves the counter be,
// so a rename/activate does not needlessly refresh live instances.
if (bumpGeneration) panel::g_panel.session->bumpBankGeneration();
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
}
// --- Promptless inner bank verbs (Q-W4 single home) ----------------------------
// Model op + persistBankOp only; NO UX. Callers own prompts/confirms/nudges. Each
// verb resolves the book fresh (panel::book(), null when no live session) and
// persists ONLY after the model accepted — a rejected op opens no undo point.
std::string bankOpCreate(const std::string& name) {
BankBook* b = panel::book();
if (!b) return {};
const std::string id = mintBankId();
if (!b->createBank(id, name)) return {}; // duplicate display name (model rule)
persistBankOp("ReaSampler: create bank");
return id;
}
bool bankOpRename(const std::string& bankId, const std::string& newName) {
BankBook* b = panel::book();
if (!b || !b->renameBank(bankId, newName)) return false; // pool / name in use
persistBankOp("ReaSampler: rename bank");
return true;
}
bool bankOpDelete(const std::string& bankId, bool bumpGeneration) {
BankBook* b = panel::book();
if (!b || !b->deleteBank(bankId)) return false; // pool un-deletable (model rule)
persistBankOp("ReaSampler: delete bank", bumpGeneration);
return true;
}
bool bankOpEvacuate(const std::string& bankId) {
BankBook* b = panel::book();
if (!b || !b->evacuate(bankId)) return false; // pool is a destination, not a source
// S9: evacuate moves members between banks (bank membership changes) -> bump.
persistBankOp("ReaSampler: evacuate bank", /*bumpGeneration=*/true);
return true;
}
bool bankOpActivate(const std::string& bankId) {
BankBook* b = panel::book();
if (!b || !b->setActiveBank(bankId)) return false; // rejects an unknown id
persistBankOp("ReaSampler: activate bank");
return true;
}
// NO-OP GUARDRAIL — VERB-AWARE (a collapse means different things per verb):
// * MOVE collapse: the source entry WAS removed (bank_book moveSample removes
// unconditionally before the dest add collapses on hash), so the index DID
// mutate — it counts toward opening an undo point.
// * COPY collapse: the source is left intact AND the dest already held the hash,
// so NOTHING changed — a true index no-op. It must NOT open an undo point.
// Hence: copy counts only real gains; move counts gains OR collapses. Ids pass
// straight to the model op — no BankModel& cached across the loop's mutations.
bool bankOpTransfer(const std::vector<std::string>& sampleIds,
const std::string& srcBankId, const std::string& destBankId,
bool copy) {
BankBook* b = panel::book();
if (!b || sampleIds.empty() || srcBankId == destBankId) return false;
if (!b->bank(srcBankId) || !b->bank(destBankId)) return false;
int ok = 0, collapsed = 0;
for (const std::string& sid : sampleIds) {
const TransferResult r =
copy ? b->copySample(sid, srcBankId, destBankId)
: b->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;
}
}
const bool mutated = copy ? (ok > 0) : (ok > 0 || collapsed > 0);
if (!mutated) return false; // nothing changed — no persist, no undo point
// S9: a move/copy changes bank membership (a sample arrives in / leaves a bank an
// instance may reference) -> bump so assigned instances refresh hands-free.
persistBankOp(copy ? "ReaSampler: copy sample(s)" : "ReaSampler: move sample(s)",
/*bumpGeneration=*/true);
return true;
}
// Index-only, this-bank scope (fork R-A: the sole surfaced verb; RemoveScope::AllBanks
// stays latent in the model). Non-destructive to the file: a last-reference remove
// leaves the file on disk, orphaned until Phase R prune — remove NEVER deletes bytes
// (the manifest is untouched). Silent: recoverability is the batched undo (R-B).
bool bankOpRemove(const std::vector<std::string>& sampleIds,
const std::string& srcBankId) {
BankBook* b = panel::book();
if (!b || sampleIds.empty() || !b->bank(srcBankId)) return false;
int removed = 0;
for (const std::string& sid : sampleIds)
if (b->removeSample(sid, srcBankId, RemoveScope::ThisBank) ==
RemoveResult::Removed)
++removed;
if (removed == 0) return false; // every id already absent — no undo point
// S9: a remove drops a sample from a bank (an instance referencing it must refresh —
// it will resolve to silence, per the stale-id policy) -> bump.
persistBankOp("ReaSampler: remove sample(s)", /*bumpGeneration=*/true);
return true;
}
// --- Selection read seam --------------------------------------------------------
std::vector<std::string> bankPanelSelectedSampleIds() {