325 lines
15 KiB
C++
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
|