// bank_actions.cpp — see bank_actions.h. // // Each mutating handler is a thin UX skin — text prompts, name resolution, console // feedback — over the promptless bankOp* verbs in shell/bank_ops (model op + // persistBankOp, one bank op = one Ctrl-Z). Pool privileges / collapse-by-hash / // active-fallback-to-pool live in bank_book; handlers only drive the verbs. // // REFERENCE-INVALIDATION GUARDRAIL: book().activeIndex() / bank()->index return a // reference INTO the book's internal vector, which a create/delete can reallocate. // No handler caches a BankModel&/Bank* across a structural mutation — ids are // resolved to strings up front and re-resolved after any create/delete. // // main.cpp owns the API pointers; this TU gets them extern. Action ids are minted // from FOREVER-STABLE strings — never change one after ship. #include "shell/actions/bank_actions.h" #include #include #include "shell/actions/action_registry.h" // channelIdFor / registerAction (shared plumbing) #include "shell/actions/prune_action.h" // doBankPruneFolder — the guarded prune body #include "core/model/bank_book.h" // BankBook, nextBankId, kPoolBankId (B1) #include "shell/bank_ops/bank_ops.h" // bankOp* promptless verbs (Q-W6 non-UI seam) #include "shell/panel/panel_bank_ops.h" // promptBankName + the panel selection seam #include "shell/panel/panel_layout.h" // full-height toggles (B3) #include "shell/persist/session.h" // ReaSamplerSession (owns book()) #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_ShowConsoleMsg #define REAPERAPI_WANT_ShowMessageBox #include "reaper_plugin_functions.h" namespace reasampler { namespace { // FOREVER-STABLE action-id SUFFIXES: the channel prefix is prepended at register // (channelCommandId); NEVER change a shipped suffix — user keybindings key off the // composed id. constexpr const char* kIdBankCreate = "BANK_CREATE"; constexpr const char* kIdBankRename = "BANK_RENAME"; constexpr const char* kIdBankDelete = "BANK_DELETE"; constexpr const char* kIdBankEvacuate = "BANK_EVACUATE"; constexpr const char* kIdBankActivateNext = "BANK_ACTIVATE_NEXT"; constexpr const char* kIdBankActivatePool = "BANK_ACTIVATE_POOL"; constexpr const char* kIdBankMoveSel = "BANK_MOVE_SELECTED"; constexpr const char* kIdBankCopySel = "BANK_COPY_SELECTED"; constexpr const char* kIdBankRemoveSel = "BANK_REMOVE_SELECTED"; constexpr const char* kIdBankPoolFull = "BANK_POOL_FULLHEIGHT"; constexpr const char* kIdBankBanksFull = "BANK_BANKS_FULLHEIGHT"; constexpr const char* kIdBankPruneFolder = "BANK_PRUNE_FOLDER"; // Not owned here; set once by bankRegisterActions. bankHandleCommand guards it // non-null before any handler runs. ReaSamplerSession* g_session = nullptr; 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_cmdBankRemoveSel = 0; int g_cmdBankPoolFull = 0; int g_cmdBankBanksFull = 0; int g_cmdBankPruneFolder = 0; // gaccel storage must outlive registration — REAPER holds each pointer until we // mirror-unregister it. One per action. 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_accelBankRemoveSel{}; gaccel_register_t g_accelBankPoolFull{}; gaccel_register_t g_accelBankBanksFull{}; gaccel_register_t g_accelBankPruneFolder{}; // Resolves a user-typed display name to a bank id ("" if none matches). UI name // resolution, not a model rule — kept here rather than the model. Unambiguous by // construction: the model enforces unique display names. std::string bankIdByDisplayName(const std::string& name) { for (const Bank& b : g_session->book().banks()) if (b.displayName == name) return b.id; return {}; } // The new bank is NOT auto-activated (create and activate are distinct acts, // mirroring capture/placement separation). void doBankCreate() { std::string name; if (!promptBankName("ReaSampler: create bank", "Bank name:", "", name)) return; if (bankOpCreate(*g_session, name).empty()) { ShowConsoleMsg( ("ReaSampler: could not create bank \"" + name + "\" (a bank with that name already exists).\n") .c_str()); } } // Two prompts (which bank, then the new name) keep this bindable form // self-contained; the panel renames in place on a tab instead. void doBankRename() { std::string which; if (!promptBankName("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 (!promptBankName("ReaSampler: rename bank", "New name:", which, newName)) return; if (!bankOpRename(*g_session, id, newName)) { ShowConsoleMsg("ReaSampler: cannot rename that bank (the pool is un-renamable, " "or another bank already uses that name).\n"); } } // If the bank holds members, confirm first (a plain delete orphans those members' // files until prune); an empty bank deletes with no prompt. void doBankDelete() { std::string which; 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()); return; } // Catch the pool BEFORE the non-empty confirm, so typing "Pool" never shows a // misleading "delete anyway?" 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 the delete. 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) } // Bump only when the deleted bank held samples — dropping them changes what a // live instance referencing one could play. if (!bankOpDelete(*g_session, id, /*bumpGeneration=*/members > 0)) { ShowConsoleMsg("ReaSampler: cannot delete that bank (the pool is un-deletable).\n"); } } // The "keep the samples" companion to delete: moves every member back to the pool. void doBankEvacuate() { std::string which; if (!promptBankName("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 (!bankOpEvacuate(*g_session, id)) { ShowConsoleMsg("ReaSampler: cannot evacuate that bank (the pool is the " "destination, not a source).\n"); } } // Cycles the active bank (pool -> named -> ... -> pool). Activating changes the // CAPTURE TARGET only — never touches the timeline. void doBankActivateNext() { std::vector 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) bankOpActivate(*g_session, target); } void doBankActivatePool() { bankOpActivate(*g_session, kPoolBankId); } // SOURCE is the bank the selection lives in (bankPanelSelectedSourceBankId), which is // NOT necessarily the active/capture-target bank — the vertical split can show a // different bank than the one active for capture. void doBankTransferSelected(bool copy) { const std::vector 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 (!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()); return; } const std::string srcId = bankPanelSelectedSourceBankId(); if (srcId == destId) { ShowConsoleMsg("ReaSampler: source and destination are the same bank.\n"); return; } bankOpTransfer(*g_session, selected, srcId, destId, copy); } // Index-only and non-destructive to the file (orphaned until prune); silent, with // the batched undo as recovery. void doBankRemoveSelected() { const std::vector selected = bankPanelSelectedSampleIds(); if (selected.empty()) { ShowConsoleMsg("ReaSampler: nothing selected in the bank panel to remove.\n"); return; } const std::string srcId = bankPanelSelectedSourceBankId(); if (g_session->book().bank(srcId) == nullptr) { ShowConsoleMsg("ReaSampler: the selection's bank no longer exists.\n"); return; } bankOpRemove(*g_session, selected, srcId); } } // namespace void bankRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) { g_session = session; // same live session as the Design View family g_cmdBankCreate = registerAction(rec, kIdBankCreate, g_accelBankCreate, "create bank"); g_cmdBankRename = registerAction(rec, kIdBankRename, g_accelBankRename, "rename bank"); g_cmdBankDelete = registerAction(rec, kIdBankDelete, g_accelBankDelete, "delete bank"); g_cmdBankEvacuate = registerAction(rec, kIdBankEvacuate, g_accelBankEvacuate, "evacuate bank to pool"); g_cmdBankActivateNext = registerAction(rec, kIdBankActivateNext, g_accelBankActivateNext, "activate next bank (cycle)"); g_cmdBankActivatePool = registerAction(rec, kIdBankActivatePool, g_accelBankActivatePool, "activate pool"); g_cmdBankMoveSel = registerAction(rec, kIdBankMoveSel, g_accelBankMoveSel, "move selected samples to bank"); g_cmdBankCopySel = registerAction(rec, kIdBankCopySel, g_accelBankCopySel, "copy selected samples to bank"); g_cmdBankRemoveSel = registerAction(rec, kIdBankRemoveSel, g_accelBankRemoveSel, "remove selected samples"); g_cmdBankPoolFull = registerAction(rec, kIdBankPoolFull, g_accelBankPoolFull, "toggle pool full-height"); g_cmdBankBanksFull = registerAction(rec, kIdBankBanksFull, g_accelBankBanksFull, "toggle banks full-height"); g_cmdBankPruneFolder = registerAction(rec, kIdBankPruneFolder, g_accelBankPruneFolder, "prune bank folder"); } 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_cmdBankRemoveSel) { doBankRemoveSelected(); return true; } if (command == g_cmdBankPoolFull) { bankPanelToggledPoolFullHeight(); return true; } if (command == g_cmdBankBanksFull) { bankPanelToggledBanksFullHeight(); return true; } if (command == g_cmdBankPruneFolder) { doBankPruneFolder(*g_session); return true; } return false; // not ours — caller's hookcommand keeps looking } int bankPruneCommandId() { return g_cmdBankPruneFolder; } void bankUnregisterActions(reaper_plugin_info_t* rec) { // Reverse of registration order; each '-command_id' re-presents the same // interned id (channelIdFor). rec->Register("-gaccel", (void*)&g_accelBankPruneFolder); rec->Register("-command_id", (void*)channelIdFor(kIdBankPruneFolder)); rec->Register("-gaccel", (void*)&g_accelBankBanksFull); rec->Register("-command_id", (void*)channelIdFor(kIdBankBanksFull)); rec->Register("-gaccel", (void*)&g_accelBankPoolFull); rec->Register("-command_id", (void*)channelIdFor(kIdBankPoolFull)); rec->Register("-gaccel", (void*)&g_accelBankRemoveSel); rec->Register("-command_id", (void*)channelIdFor(kIdBankRemoveSel)); rec->Register("-gaccel", (void*)&g_accelBankCopySel); rec->Register("-command_id", (void*)channelIdFor(kIdBankCopySel)); rec->Register("-gaccel", (void*)&g_accelBankMoveSel); rec->Register("-command_id", (void*)channelIdFor(kIdBankMoveSel)); rec->Register("-gaccel", (void*)&g_accelBankActivatePool); rec->Register("-command_id", (void*)channelIdFor(kIdBankActivatePool)); rec->Register("-gaccel", (void*)&g_accelBankActivateNext); rec->Register("-command_id", (void*)channelIdFor(kIdBankActivateNext)); rec->Register("-gaccel", (void*)&g_accelBankEvacuate); rec->Register("-command_id", (void*)channelIdFor(kIdBankEvacuate)); rec->Register("-gaccel", (void*)&g_accelBankDelete); rec->Register("-command_id", (void*)channelIdFor(kIdBankDelete)); rec->Register("-gaccel", (void*)&g_accelBankRename); rec->Register("-command_id", (void*)channelIdFor(kIdBankRename)); rec->Register("-gaccel", (void*)&g_accelBankCreate); rec->Register("-command_id", (void*)channelIdFor(kIdBankCreate)); g_session = nullptr; } } // namespace reasampler