feat(banks): bindable multi-bank action family — create/rename/delete/evacuate/activate/move/copy + full-height toggles (B3)
This commit is contained in:
+360
-1
@@ -24,7 +24,9 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "persist.h" // ReaSamplerSession (owns view() model)
|
||||
#include "bank_book.h" // BankBook, nextBankId, TransferResult, kPoolBankId (B1)
|
||||
#include "bank_panel.h" // selection seam + full-height toggles (B3/B4)
|
||||
#include "persist.h" // ReaSamplerSession (owns book() + view() model)
|
||||
#include "track_guid.h" // shared MediaTrack* -> canonical GUID key
|
||||
#include "view.h" // applyMode (D2 shell)
|
||||
#include "view_mode_model.h"
|
||||
@@ -37,6 +39,10 @@
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_Main_SaveProject
|
||||
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||
#define REAPERAPI_WANT_GetUserInputs
|
||||
#define REAPERAPI_WANT_ShowMessageBox
|
||||
#define REAPERAPI_WANT_genGuid
|
||||
#define REAPERAPI_WANT_guidToString
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
@@ -271,4 +277,357 @@ void designViewUnregisterActions(reaper_plugin_info_t* rec) {
|
||||
g_session = nullptr;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Multi-bank action family (Phase B3)
|
||||
// ===========================================================================
|
||||
//
|
||||
// Each action drives the B1 model on g_session->book() and persists via
|
||||
// g_session->saveToActiveProject() so the change travels with the .rpp — exactly as
|
||||
// the capture path persists a new Sample (main.cpp RunCapture). The book's rules
|
||||
// (pool privileges, collapse-by-hash, active-fallback-to-pool) all live in bank_book;
|
||||
// these handlers only call the model and react to the boolean / TransferResult.
|
||||
//
|
||||
// REFERENCE-INVALIDATION GUARDRAIL (B2 review): book().activeIndex() / bank()->index
|
||||
// return a reference INTO the book's internal vector, which a create/delete can
|
||||
// reallocate. No handler here caches a BankIndex& (or a Bank*) across a structural
|
||||
// mutation — each resolves ids to strings up front and re-resolves after any
|
||||
// create/delete. Move/copy pass ids (not references) straight to moveSample/copySample.
|
||||
|
||||
namespace {
|
||||
|
||||
// FOREVER-STABLE multi-bank action-id strings. Same CEREBELLUM_REASAMPLER_ family
|
||||
// prefix; each is minted into a persistent command id user keybindings key off —
|
||||
// NEVER change these after ship.
|
||||
constexpr const char* kIdBankCreate = "CEREBELLUM_REASAMPLER_BANK_CREATE";
|
||||
constexpr const char* kIdBankRename = "CEREBELLUM_REASAMPLER_BANK_RENAME";
|
||||
constexpr const char* kIdBankDelete = "CEREBELLUM_REASAMPLER_BANK_DELETE";
|
||||
constexpr const char* kIdBankEvacuate = "CEREBELLUM_REASAMPLER_BANK_EVACUATE";
|
||||
constexpr const char* kIdBankActivateNext = "CEREBELLUM_REASAMPLER_BANK_ACTIVATE_NEXT";
|
||||
constexpr const char* kIdBankActivatePool = "CEREBELLUM_REASAMPLER_BANK_ACTIVATE_POOL";
|
||||
constexpr const char* kIdBankMoveSel = "CEREBELLUM_REASAMPLER_BANK_MOVE_SELECTED";
|
||||
constexpr const char* kIdBankCopySel = "CEREBELLUM_REASAMPLER_BANK_COPY_SELECTED";
|
||||
constexpr const char* kIdBankPoolFull = "CEREBELLUM_REASAMPLER_BANK_POOL_FULLHEIGHT";
|
||||
constexpr const char* kIdBankBanksFull = "CEREBELLUM_REASAMPLER_BANK_BANKS_FULLHEIGHT";
|
||||
|
||||
int g_cmdBankCreate = 0;
|
||||
int g_cmdBankRename = 0;
|
||||
int g_cmdBankDelete = 0;
|
||||
int g_cmdBankEvacuate = 0;
|
||||
int g_cmdBankActivateNext = 0;
|
||||
int g_cmdBankActivatePool = 0;
|
||||
int g_cmdBankMoveSel = 0;
|
||||
int g_cmdBankCopySel = 0;
|
||||
int g_cmdBankPoolFull = 0;
|
||||
int g_cmdBankBanksFull = 0;
|
||||
|
||||
gaccel_register_t g_accelBankCreate{};
|
||||
gaccel_register_t g_accelBankRename{};
|
||||
gaccel_register_t g_accelBankDelete{};
|
||||
gaccel_register_t g_accelBankEvacuate{};
|
||||
gaccel_register_t g_accelBankActivateNext{};
|
||||
gaccel_register_t g_accelBankActivatePool{};
|
||||
gaccel_register_t g_accelBankMoveSel{};
|
||||
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.
|
||||
void persistBook() { g_session->saveToActiveProject(); }
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
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())))
|
||||
return false; // user cancelled
|
||||
std::string s(buf.data());
|
||||
if (s.empty()) return false; // an empty name is not a valid bank name
|
||||
out = std::move(s);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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.
|
||||
std::string bankIdByDisplayName(const std::string& name) {
|
||||
for (const Bank& b : g_session->book().banks())
|
||||
if (b.displayName == name) return b.id;
|
||||
return {};
|
||||
}
|
||||
|
||||
// -- Action bodies ---------------------------------------------------------
|
||||
|
||||
// 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.
|
||||
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");
|
||||
return;
|
||||
}
|
||||
persistBook();
|
||||
ShowConsoleMsg(("ReaSampler: created bank \"" + name + "\".\n").c_str());
|
||||
}
|
||||
|
||||
// Rename a bank: prompt for which bank (by current display name) and the new name.
|
||||
// The pool is un-renamable (the model rejects it). Two prompts keep the bindable form
|
||||
// self-contained; B4's panel renames in place on a tab.
|
||||
void doBankRename() {
|
||||
std::string which;
|
||||
if (!promptText("ReaSampler: rename bank", "Bank to rename (current name):", "",
|
||||
which))
|
||||
return;
|
||||
const std::string id = bankIdByDisplayName(which);
|
||||
if (id.empty()) {
|
||||
ShowConsoleMsg(("ReaSampler: no bank named \"" + which + "\".\n").c_str());
|
||||
return;
|
||||
}
|
||||
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");
|
||||
return;
|
||||
}
|
||||
persistBook();
|
||||
ShowConsoleMsg(("ReaSampler: renamed \"" + which + "\" -> \"" + newName + "\".\n")
|
||||
.c_str());
|
||||
}
|
||||
|
||||
// Delete a named bank. Bindable safe-form of the confirm-on-non-empty guardrail:
|
||||
// prompt for the bank; if it holds members, a YESNO ShowMessageBox names evacuate as
|
||||
// the alternative before dropping them (a plain delete orphans those members' files
|
||||
// until prune — CONTEXT.md §delete). An empty bank deletes with no prompt. The richer
|
||||
// panel confirm (naming evacuate inline, with a one-click evacuate) arrives in B4.
|
||||
void doBankDelete() {
|
||||
std::string which;
|
||||
if (!promptText("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());
|
||||
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);
|
||||
if (!b) return; // race-safe: id resolved above but re-check
|
||||
const std::size_t members = b->index.size();
|
||||
if (members > 0) {
|
||||
const std::string msg =
|
||||
"\"" + which + "\" holds " + std::to_string(members) +
|
||||
(members == 1 ? " sample" : " samples") +
|
||||
".\n\nDeleting drops them from every bank (their files are NOT deleted, "
|
||||
"but no bank will reference them until prune).\n\nTo keep the samples, "
|
||||
"cancel and Evacuate the bank to the pool first.\n\nDelete anyway?";
|
||||
const int r = ShowMessageBox(msg.c_str(), "ReaSampler: delete non-empty bank", 4);
|
||||
if (r != 6) return; // 6 == YES; anything else cancels (SDK ~6544)
|
||||
}
|
||||
if (!g_session->book().deleteBank(id)) {
|
||||
ShowConsoleMsg("ReaSampler: cannot delete that bank (the pool is un-deletable).\n");
|
||||
return;
|
||||
}
|
||||
persistBook();
|
||||
ShowConsoleMsg(("ReaSampler: deleted bank \"" + which + "\".\n").c_str());
|
||||
}
|
||||
|
||||
// Evacuate a named bank: move every member back to the pool (index-only, collapse by
|
||||
// hash), leaving the bank empty. The pool is un-evacuable (the model rejects it). The
|
||||
// intended "keep the samples" companion to delete.
|
||||
void doBankEvacuate() {
|
||||
std::string which;
|
||||
if (!promptText("ReaSampler: evacuate bank", "Bank to evacuate to the pool:", "",
|
||||
which))
|
||||
return;
|
||||
const std::string id = bankIdByDisplayName(which);
|
||||
if (id.empty()) {
|
||||
ShowConsoleMsg(("ReaSampler: no bank named \"" + which + "\".\n").c_str());
|
||||
return;
|
||||
}
|
||||
if (!g_session->book().evacuate(id)) {
|
||||
ShowConsoleMsg("ReaSampler: cannot evacuate that bank (the pool is the "
|
||||
"destination, not a source).\n");
|
||||
return;
|
||||
}
|
||||
persistBook();
|
||||
ShowConsoleMsg(("ReaSampler: evacuated \"" + which + "\" to the pool.\n").c_str());
|
||||
}
|
||||
|
||||
// Cycle the active bank forward in ordinal order (pool -> named -> ... -> pool),
|
||||
// via the pure nextBankId helper. Activating a bank changes the CAPTURE TARGET (the
|
||||
// next capture lands in the newly-active bank — B2's book().activeIndex() seam) and
|
||||
// never touches the timeline. Persist so the active id travels with the .rpp.
|
||||
void doBankActivateNext() {
|
||||
std::vector<std::string> ids;
|
||||
ids.reserve(g_session->book().size());
|
||||
for (const Bank& b : g_session->book().banks()) ids.push_back(b.id);
|
||||
const std::string target = nextBankId(ids, g_session->book().activeBankId());
|
||||
if (target.empty()) return; // degenerate (no banks) — cannot happen (pool seeded)
|
||||
if (!g_session->book().setActiveBank(target)) return;
|
||||
persistBook();
|
||||
const Bank* b = g_session->book().bank(target);
|
||||
ShowConsoleMsg(("ReaSampler: active bank -> \"" +
|
||||
(b ? b->displayName : target) + "\".\n")
|
||||
.c_str());
|
||||
}
|
||||
|
||||
// Activate the pool directly (the common "back to the default target" jump). Bindable
|
||||
// direct-by-id form; a general activate-bank-by-name/menu is a B4 affordance.
|
||||
void doBankActivatePool() {
|
||||
if (!g_session->book().setActiveBank(kPoolBankId)) return;
|
||||
persistBook();
|
||||
ShowConsoleMsg("ReaSampler: active bank -> \"Pool\".\n");
|
||||
}
|
||||
|
||||
// Move or copy the panel's selected samples from the ACTIVE bank into a named
|
||||
// destination bank (prompted by display name). The panel grid shows the active bank,
|
||||
// so its selection ids are members of the active bank — that is the source. Both are
|
||||
// index-only (files never relocate); move removes the source entry, copy retains it;
|
||||
// both observe destination collapse-by-hash (bank_book). B4's "move to bank" menu will
|
||||
// drive moveSample/copySample directly with a menu-chosen destination — this bindable
|
||||
// form is the same operation with a text-prompt destination.
|
||||
void doBankTransferSelected(bool copy) {
|
||||
const std::vector<std::string> selected = bankPanelSelectedSampleIds();
|
||||
if (selected.empty()) {
|
||||
ShowConsoleMsg("ReaSampler: nothing selected in the bank panel to "
|
||||
"move/copy.\n");
|
||||
return;
|
||||
}
|
||||
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;
|
||||
const std::string destId = bankIdByDisplayName(destName);
|
||||
if (destId.empty()) {
|
||||
ShowConsoleMsg(("ReaSampler: no bank named \"" + destName + "\".\n").c_str());
|
||||
return;
|
||||
}
|
||||
// Source = the active bank (what the panel grid shows). Pass ids by value — no
|
||||
// BankIndex& is cached across the loop's mutations.
|
||||
const std::string srcId = g_session->book().activeBankId();
|
||||
if (srcId == destId) {
|
||||
ShowConsoleMsg("ReaSampler: source and destination are the same bank.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
int ok = 0, collapsed = 0, absent = 0;
|
||||
for (const std::string& sampleId : selected) {
|
||||
const TransferResult r =
|
||||
copy ? g_session->book().copySample(sampleId, srcId, destId)
|
||||
: g_session->book().moveSample(sampleId, srcId, destId);
|
||||
switch (r) {
|
||||
case TransferResult::Moved:
|
||||
case TransferResult::Copied: ++ok; break;
|
||||
case TransferResult::Collapsed: ++collapsed; break;
|
||||
case TransferResult::RejectedSampleAbsent: ++absent; break;
|
||||
// Unknown-bank / same-bank are pre-checked above; treat defensively as no-ops.
|
||||
case TransferResult::RejectedUnknownBank:
|
||||
case TransferResult::RejectedSameBank: break;
|
||||
}
|
||||
}
|
||||
persistBook();
|
||||
std::string log = std::string("ReaSampler: ") + verb + " -> \"" + destName +
|
||||
"\": " + std::to_string(ok) + " " + verb + "d";
|
||||
if (collapsed) log += ", " + std::to_string(collapsed) + " collapsed on hash";
|
||||
if (absent) log += ", " + std::to_string(absent) + " no longer present";
|
||||
log += ".\n";
|
||||
ShowConsoleMsg(log.c_str());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void bankRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) {
|
||||
g_session = session; // shared with the Design View family; same live session
|
||||
|
||||
g_cmdBankCreate = registerAction(rec, kIdBankCreate, g_accelBankCreate,
|
||||
"ReaSampler: create bank");
|
||||
g_cmdBankRename = registerAction(rec, kIdBankRename, g_accelBankRename,
|
||||
"ReaSampler: rename bank");
|
||||
g_cmdBankDelete = registerAction(rec, kIdBankDelete, g_accelBankDelete,
|
||||
"ReaSampler: delete bank");
|
||||
g_cmdBankEvacuate = registerAction(rec, kIdBankEvacuate, g_accelBankEvacuate,
|
||||
"ReaSampler: evacuate bank to pool");
|
||||
g_cmdBankActivateNext = registerAction(rec, kIdBankActivateNext, g_accelBankActivateNext,
|
||||
"ReaSampler: activate next bank (cycle)");
|
||||
g_cmdBankActivatePool = registerAction(rec, kIdBankActivatePool, g_accelBankActivatePool,
|
||||
"ReaSampler: activate pool");
|
||||
g_cmdBankMoveSel = registerAction(rec, kIdBankMoveSel, g_accelBankMoveSel,
|
||||
"ReaSampler: move selected samples to bank");
|
||||
g_cmdBankCopySel = registerAction(rec, kIdBankCopySel, g_accelBankCopySel,
|
||||
"ReaSampler: copy selected samples to bank");
|
||||
g_cmdBankPoolFull = registerAction(rec, kIdBankPoolFull, g_accelBankPoolFull,
|
||||
"ReaSampler: toggle pool full-height");
|
||||
g_cmdBankBanksFull = registerAction(rec, kIdBankBanksFull, g_accelBankBanksFull,
|
||||
"ReaSampler: toggle banks full-height");
|
||||
}
|
||||
|
||||
bool bankHandleCommand(int command) {
|
||||
if (command == 0 || !g_session) return false;
|
||||
|
||||
if (command == g_cmdBankCreate) { doBankCreate(); return true; }
|
||||
if (command == g_cmdBankRename) { doBankRename(); return true; }
|
||||
if (command == g_cmdBankDelete) { doBankDelete(); return true; }
|
||||
if (command == g_cmdBankEvacuate) { doBankEvacuate(); return true; }
|
||||
if (command == g_cmdBankActivateNext) { doBankActivateNext(); return true; }
|
||||
if (command == g_cmdBankActivatePool) { doBankActivatePool(); return true; }
|
||||
if (command == g_cmdBankMoveSel) { doBankTransferSelected(false); return true; }
|
||||
if (command == g_cmdBankCopySel) { doBankTransferSelected(true); return true; }
|
||||
if (command == g_cmdBankPoolFull) { bankPanelToggledPoolFullHeight(); return true; }
|
||||
if (command == g_cmdBankBanksFull) { bankPanelToggledBanksFullHeight(); return true; }
|
||||
|
||||
return false; // not ours — caller's hookcommand keeps looking
|
||||
}
|
||||
|
||||
void bankUnregisterActions(reaper_plugin_info_t* rec) {
|
||||
// Mirror-unregister with '-'-prefixed strings, reverse of registration order.
|
||||
rec->Register("-gaccel", (void*)&g_accelBankBanksFull);
|
||||
rec->Register("-command_id", (void*)kIdBankBanksFull);
|
||||
rec->Register("-gaccel", (void*)&g_accelBankPoolFull);
|
||||
rec->Register("-command_id", (void*)kIdBankPoolFull);
|
||||
rec->Register("-gaccel", (void*)&g_accelBankCopySel);
|
||||
rec->Register("-command_id", (void*)kIdBankCopySel);
|
||||
rec->Register("-gaccel", (void*)&g_accelBankMoveSel);
|
||||
rec->Register("-command_id", (void*)kIdBankMoveSel);
|
||||
rec->Register("-gaccel", (void*)&g_accelBankActivatePool);
|
||||
rec->Register("-command_id", (void*)kIdBankActivatePool);
|
||||
rec->Register("-gaccel", (void*)&g_accelBankActivateNext);
|
||||
rec->Register("-command_id", (void*)kIdBankActivateNext);
|
||||
rec->Register("-gaccel", (void*)&g_accelBankEvacuate);
|
||||
rec->Register("-command_id", (void*)kIdBankEvacuate);
|
||||
rec->Register("-gaccel", (void*)&g_accelBankDelete);
|
||||
rec->Register("-command_id", (void*)kIdBankDelete);
|
||||
rec->Register("-gaccel", (void*)&g_accelBankRename);
|
||||
rec->Register("-command_id", (void*)kIdBankRename);
|
||||
rec->Register("-gaccel", (void*)&g_accelBankCreate);
|
||||
rec->Register("-command_id", (void*)kIdBankCreate);
|
||||
|
||||
// g_session is shared with the Design View family; designViewUnregisterActions
|
||||
// also nulls it. Nulling twice is harmless. Leave it to whichever runs last.
|
||||
g_session = nullptr;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -39,4 +39,31 @@ bool designViewHandleCommand(int command);
|
||||
// '-'-prefixed strings (per the contract's unload rule). Call once on rec==nullptr.
|
||||
void designViewUnregisterActions(reaper_plugin_info_t* rec);
|
||||
|
||||
// --- Multi-bank action family (Phase B3) -----------------------------------
|
||||
// The bindable action set that drives the multi-bank workflow: create / rename /
|
||||
// delete / evacuate a bank, activate a bank (direct pool/design-free + cycle), move /
|
||||
// copy the panel's selected samples into a bank, and the two vertical-split
|
||||
// full-height toggles. Every mutating action drives the B1 model on
|
||||
// g_session.book() and persists via g_session.saveToActiveProject() so the change
|
||||
// travels with the .rpp; the toggles flip the B4-rendered layout bit on the panel.
|
||||
//
|
||||
// Same registration/routing/unload contract as the Design View family above and the
|
||||
// same shared g_session. Kept a distinct trio (not folded into the Design View one)
|
||||
// because the two families are orthogonal pillars — but they share the single
|
||||
// hookcommand main.cpp owns; each family's Handle claims only its own ids.
|
||||
|
||||
// Registers the multi-bank family against `rec`. `session` is the live session (must
|
||||
// outlive registration). Call exactly once at load. Shares g_session with the Design
|
||||
// View family — pass the SAME session pointer.
|
||||
void bankRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session);
|
||||
|
||||
// Services one fired command for the multi-bank family. True iff it was one of this
|
||||
// family's ids (and handled); false otherwise so the caller's hookcommand keeps
|
||||
// looking. Safe for any command.
|
||||
bool bankHandleCommand(int command);
|
||||
|
||||
// Mirror-unregisters the multi-bank family with '-'-prefixed strings. Call once on
|
||||
// rec==nullptr (before g_session is torn down).
|
||||
void bankUnregisterActions(reaper_plugin_info_t* rec);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -668,6 +668,22 @@ std::optional<BankBook> BankBook::deserialize(const std::string& json) {
|
||||
return book;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Active-bank cycle ordering (pure, free function — mirror of nextModeId)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::string nextBankId(const std::vector<std::string>& orderedBankIds,
|
||||
const std::string& currentBankId) {
|
||||
if (orderedBankIds.empty()) return {}; // nothing to cycle to
|
||||
for (std::size_t i = 0; i < orderedBankIds.size(); ++i) {
|
||||
if (orderedBankIds[i] == currentBankId)
|
||||
return orderedBankIds[(i + 1) % orderedBankIds.size()]; // wrap past the last
|
||||
}
|
||||
// Active id not in the list (stale/unknown) — jump to the first id as a sane
|
||||
// home rather than returning "" (matches nextModeId's fallback).
|
||||
return orderedBankIds.front();
|
||||
}
|
||||
|
||||
BankBook BankBook::loadFromPersisted(const std::string& banksJson,
|
||||
const std::string& legacyJson) {
|
||||
// Precedence 1: the authoritative `banks` blob. A present-but-malformed blob is
|
||||
|
||||
@@ -221,4 +221,17 @@ private:
|
||||
void adoptBanks(std::vector<Bank>&& banks, const std::string& activeBank);
|
||||
};
|
||||
|
||||
// The next bank id to activate when cycling the active bank forward, in ordinal
|
||||
// order (the ids arrive pool-first, named 1..N, matching banks()). Wraps: the id
|
||||
// after the last returns the first (pool → named → … → pool). This is the pure
|
||||
// decision behind the "cycle active bank" action — the shell reads the book's
|
||||
// ordered bank ids + current active id, asks for the next, and activates it.
|
||||
// * empty list -> "" (nothing to cycle to)
|
||||
// * single id (pool-only) -> that id (a one-bank book stays put)
|
||||
// * currentBankId not present -> the first id (a sane home to jump to)
|
||||
// Exposed as a free function (not a BankBook member) so it is unit-testable against
|
||||
// a bare id vector without a full book. Mirror of view_mode_model's nextModeId.
|
||||
std::string nextBankId(const std::vector<std::string>& orderedBankIds,
|
||||
const std::string& currentBankId);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -194,6 +194,13 @@ struct PanelState {
|
||||
// persistence is a noted follow-on. Mutated ONLY by a click in the footer strip.
|
||||
TailSetting tail;
|
||||
|
||||
// --- Vertical-split full-height layout (Phase B3) -------------------------
|
||||
// Which region(s) the vertical split shows: both (Split, default), pool only,
|
||||
// or named-banks only. B3 actions flip it (bankPanelToggled*FullHeight); B4's
|
||||
// panel renders from it. In-memory only (a UI-layout preference, not project
|
||||
// state — it must not travel with the .rpp); resets to Split on unload.
|
||||
BankPanelFullHeight fullHeight = BankPanelFullHeight::Split;
|
||||
|
||||
// --- Audition preview (Wave B) --------------------------------------------
|
||||
//
|
||||
// The stock preview register we hand to PlayPreview/StopPreview. Its cs/mutex
|
||||
@@ -999,6 +1006,30 @@ TailSetting bankPanelTailSetting() {
|
||||
return s;
|
||||
}
|
||||
|
||||
BankPanelFullHeight bankPanelFullHeight() {
|
||||
// In-memory for the extension's lifetime (g_panel is static), like the tail
|
||||
// setting: survives panel open/close and bank changes, resets to Split on unload.
|
||||
return g_panel.fullHeight;
|
||||
}
|
||||
|
||||
// Shared toggle body: enter `target` from any other state, or fall back to Split when
|
||||
// already at `target` (a second press restores the split). Requests a repaint via the
|
||||
// same InvalidateRect the refresh path uses, so an open panel reflects the change; a
|
||||
// closed panel (hwnd null) simply stores the bit for B4 to render when it opens.
|
||||
static void setFullHeight(BankPanelFullHeight target) {
|
||||
g_panel.fullHeight =
|
||||
(g_panel.fullHeight == target) ? BankPanelFullHeight::Split : target;
|
||||
if (g_panel.hwnd) InvalidateRect(g_panel.hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void bankPanelToggledPoolFullHeight() {
|
||||
setFullHeight(BankPanelFullHeight::PoolOnly);
|
||||
}
|
||||
|
||||
void bankPanelToggledBanksFullHeight() {
|
||||
setFullHeight(BankPanelFullHeight::BanksOnly);
|
||||
}
|
||||
|
||||
void bankPanelShutdown() {
|
||||
closePanel(); // stops audition + destroys the window
|
||||
deinitPreview(); // destroy the preview lock (after the last stop)
|
||||
|
||||
@@ -61,6 +61,36 @@ void bankPanelRefresh();
|
||||
// state only; the toggle is mutated by a click inside the panel, never here.
|
||||
TailSetting bankPanelTailSetting();
|
||||
|
||||
// The vertical-split full-height layout state (Phase B). The bank window splits
|
||||
// vertically — pool on top, named-banks region below — and two toggles collapse the
|
||||
// split: pool full-height (hide the named-banks region) and banks full-height (hide
|
||||
// the pool). The two are mutually exclusive with the default (both regions shown),
|
||||
// so one enum captures the whole state.
|
||||
//
|
||||
// This bit is B3-owned (the actions flip it); B4's panel RENDERS from it. It lives
|
||||
// here beside the tail setting — the other session-level view-layout bit the panel
|
||||
// reads — NOT in the persisted ReaSamplerSession: it is a UI-layout preference, not
|
||||
// project state, so it must not travel with the .rpp. In-memory for the extension's
|
||||
// lifetime; resets to Split on unload.
|
||||
enum class BankPanelFullHeight {
|
||||
Split, // default: pool region on top, named-banks region below
|
||||
PoolOnly, // pool full-height — named-banks region hidden
|
||||
BanksOnly, // banks full-height — pool region hidden
|
||||
};
|
||||
|
||||
// The current full-height layout state (default Split). READ by B4's panel to decide
|
||||
// which region(s) to draw. Safe before the panel has ever opened.
|
||||
BankPanelFullHeight bankPanelFullHeight();
|
||||
|
||||
// Toggles pool full-height: Split <-> PoolOnly. From PoolOnly returns to Split; from
|
||||
// either other state (Split or BanksOnly) enters PoolOnly. Bound to the "pool
|
||||
// full-height" action. Requests a repaint so an open panel reflects the change.
|
||||
void bankPanelToggledPoolFullHeight();
|
||||
|
||||
// Toggles banks full-height: Split <-> BanksOnly, symmetric to the pool toggle.
|
||||
// Bound to the "banks full-height" action. Requests a repaint.
|
||||
void bankPanelToggledBanksFullHeight();
|
||||
|
||||
// Tears the panel down on extension unload: destroys the window and releases any
|
||||
// cached thumbnails / PCM handles. Mirror of bankPanelInit; safe if never opened.
|
||||
void bankPanelShutdown();
|
||||
|
||||
@@ -714,6 +714,8 @@ static bool OnHookCommand(int command, int /*flag*/)
|
||||
// Design View action family (D4). Claims only its own ids; returns false for the
|
||||
// rest so this hook keeps looking (per the contract).
|
||||
if (reasampler::designViewHandleCommand(command)) return true;
|
||||
// Multi-bank action family (B3). Same contract: claims only its own ids.
|
||||
if (reasampler::bankHandleCommand(command)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -761,6 +763,8 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
// Tear down the Design View action family (D4) — mirror-unregisters each
|
||||
// gaccel + command_id with '-'-prefixed strings. After the hook is gone.
|
||||
reasampler::designViewUnregisterActions(g_rec);
|
||||
// Tear down the multi-bank action family (B3) — same mirror-unregister.
|
||||
reasampler::bankUnregisterActions(g_rec);
|
||||
g_rec->Register("-gaccel", (void*)&g_accelCancelRealtime);
|
||||
g_rec->Register("-command_id",
|
||||
(void*)(REASAMPLER_ACTION_PREFIX "CANCEL_REALTIME_CAPTURE"));
|
||||
@@ -910,6 +914,12 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
// the hook so every id is minted first.
|
||||
reasampler::designViewRegisterActions(rec, &g_session);
|
||||
|
||||
// Register the multi-bank action family (B3): create/rename/delete/evacuate bank,
|
||||
// activate (cycle + pool), move/copy selected samples to a bank, and the two
|
||||
// full-height layout toggles. Shares g_session with the Design View family; routed
|
||||
// by the same hookcommand via bankHandleCommand. Registered before the hook.
|
||||
reasampler::bankRegisterActions(rec, &g_session);
|
||||
|
||||
// One hookcommand routes every ReaSampler action (spike + toggle + Design View).
|
||||
// Registered once, after all command ids are minted.
|
||||
rec->Register("hookcommand", (void*)&OnHookCommand);
|
||||
|
||||
@@ -405,6 +405,60 @@ static void testLoadMalformedBanksDegradesWithoutLegacyFallback() {
|
||||
CHECK(loaded.pool().index.query("id-stale") == nullptr); // legacy did not leak
|
||||
}
|
||||
|
||||
// --- B3: active-bank cycle ordering (pure free function) -------------------
|
||||
// nextBankId(orderedIds, current) is the pure decision behind the "cycle active
|
||||
// bank" action: given the book's ordered bank ids (pool-first) + the current active
|
||||
// id, return the next id in ordinal order, wrapping pool -> named -> ... -> pool.
|
||||
|
||||
static void testCycleOrderingWrapAround() {
|
||||
// pool -> drums -> hits -> (wrap) pool. Exercises every step + the wrap.
|
||||
const std::vector<std::string> ids = {kPoolBankId, "drums", "hits"};
|
||||
CHECK(nextBankId(ids, kPoolBankId) == "drums");
|
||||
CHECK(nextBankId(ids, "drums") == "hits");
|
||||
CHECK(nextBankId(ids, "hits") == std::string(kPoolBankId)); // wrap past the last
|
||||
}
|
||||
|
||||
static void testCyclePoolOnlyStaysPool() {
|
||||
// A pool-only book (no named banks) cycles to itself — the single id wraps to
|
||||
// itself. The action becomes a no-op activation, which is correct.
|
||||
const std::vector<std::string> ids = {kPoolBankId};
|
||||
CHECK(nextBankId(ids, kPoolBankId) == std::string(kPoolBankId));
|
||||
}
|
||||
|
||||
static void testCycleUnknownActiveResolvesToFirst() {
|
||||
// A stale/unknown active id (e.g. the active bank was just deleted and the
|
||||
// ordered list already dropped it) resolves to the first id — a sane home to jump
|
||||
// to rather than "" — matching nextModeId's fallback.
|
||||
const std::vector<std::string> ids = {kPoolBankId, "drums"};
|
||||
CHECK(nextBankId(ids, "ghost") == std::string(kPoolBankId));
|
||||
}
|
||||
|
||||
static void testCycleEmptyListYieldsEmpty() {
|
||||
// Degenerate guard: an empty list has nothing to cycle to. (A real BankBook always
|
||||
// seeds the pool, so this cannot arise from the book — but the pure helper must not
|
||||
// index into an empty vector.)
|
||||
const std::vector<std::string> ids;
|
||||
CHECK(nextBankId(ids, kPoolBankId).empty());
|
||||
}
|
||||
|
||||
static void testCycleMatchesBookOrdinalOrder() {
|
||||
// Integration-flavoured but still pure: drive the cycle off a real book's banks()
|
||||
// order and confirm one full loop lands back on the pool, activating each bank in
|
||||
// ordinal order. This is exactly what the action does (build ids from banks(),
|
||||
// call nextBankId, setActiveBank).
|
||||
BankBook book;
|
||||
CHECK(book.createBank("a", "A"));
|
||||
CHECK(book.createBank("b", "B")); // ordinals: pool 0, a 1, b 2
|
||||
|
||||
std::vector<std::string> ids;
|
||||
for (const Bank& bk : book.banks()) ids.push_back(bk.id);
|
||||
|
||||
std::string cur = book.activeBankId(); // pool
|
||||
cur = nextBankId(ids, cur); CHECK(cur == "a");
|
||||
cur = nextBankId(ids, cur); CHECK(cur == "b");
|
||||
cur = nextBankId(ids, cur); CHECK(cur == std::string(kPoolBankId)); // full loop
|
||||
}
|
||||
|
||||
static void testActiveBankResolveAfterCorruptPersistedId() {
|
||||
// A book blob whose activeBank names no bank resolves to the pool (defensive).
|
||||
const char* json =
|
||||
@@ -435,6 +489,11 @@ int main() {
|
||||
testLoadEmptyWhenNeither();
|
||||
testLoadMalformedBanksDegradesWithoutLegacyFallback();
|
||||
testActiveBankResolveAfterCorruptPersistedId();
|
||||
testCycleOrderingWrapAround();
|
||||
testCyclePoolOnlyStaysPool();
|
||||
testCycleUnknownActiveResolvesToFirst();
|
||||
testCycleEmptyListYieldsEmpty();
|
||||
testCycleMatchesBookOrdinalOrder();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
|
||||
Reference in New Issue
Block a user