Files
reasampler/src/shell/actions/bank_actions.cpp
T

325 lines
15 KiB
C++

// 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 <string>
#include <vector>
#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<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)
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<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 (!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<std::string> 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