a67a2f9479
Pool grid on top, LICE-drawn named-banks tab strip below with overflow-scroll (new pure tab_strip seam, unit-tested). Full-height toggles, unmistakable active-bank readout distinct from the shown tab, tab context menu (activate/rename/delete/evacuate/create) with rich confirm-on-non-empty-delete, and move/copy via menu + drag with drop-highlighting. Fold-in: deserialize auto-disambiguates duplicate folded bank names instead of rejecting the book.
663 lines
33 KiB
C++
663 lines
33 KiB
C++
// actions.cpp — the Design View action family (Phase D4). See actions.h.
|
|
//
|
|
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
|
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API pointers
|
|
// (CLAUDE.md §contract). The action ids are minted from FOREVER-STABLE strings (the
|
|
// same CEREBELLUM_REASAMPLER_ family prefix main.cpp uses); user keybindings key off
|
|
// them, so they must never change after ship.
|
|
//
|
|
// Each action:
|
|
// 1. mutates the session's ViewModeModel (membership tag/untag/show-both, or the
|
|
// active mode via toggle/activate) — the pure D1 state,
|
|
// 2. reapplies the active mode through the D2 view shell (applyMode) so the change
|
|
// takes visible effect immediately (tagging a track into Design while in Arrange
|
|
// parks it right away; a mode change re-partitions and re-parks in one step).
|
|
//
|
|
// Selection-driven mutations iterate the CURRENT REAPER track selection
|
|
// (CountSelectedTracks/GetSelectedTrack — both ignore the master, which is correct:
|
|
// the master is never tagged) and resolve each track to its canonical GUID key via
|
|
// the shared guidString helper, so the keys match exactly what the D2 shell / view
|
|
// tree key on (the cross-module key contract).
|
|
|
|
#include "actions.h"
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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"
|
|
|
|
#include "reaper_plugin.h" // reaper_plugin_info_t, gaccel_register_t (full defs)
|
|
|
|
#define REAPERAPI_MINIMAL
|
|
#define REAPERAPI_WANT_CountSelectedTracks
|
|
#define REAPERAPI_WANT_GetSelectedTrack
|
|
#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 {
|
|
|
|
namespace {
|
|
|
|
// FOREVER-STABLE action-id strings. Same family prefix as main.cpp's capture/panel
|
|
// actions; each full string is minted into a persistent command id and user
|
|
// keybindings key off it — NEVER change these after ship.
|
|
constexpr const char* kIdToggleMode = "CEREBELLUM_REASAMPLER_VIEW_TOGGLE_MODE";
|
|
constexpr const char* kIdActivateArrange = "CEREBELLUM_REASAMPLER_VIEW_ACTIVATE_ARRANGE";
|
|
constexpr const char* kIdActivateDesign = "CEREBELLUM_REASAMPLER_VIEW_ACTIVATE_DESIGN";
|
|
constexpr const char* kIdTagDesign = "CEREBELLUM_REASAMPLER_VIEW_TAG_DESIGN";
|
|
constexpr const char* kIdTagArrange = "CEREBELLUM_REASAMPLER_VIEW_TAG_ARRANGE";
|
|
constexpr const char* kIdUntag = "CEREBELLUM_REASAMPLER_VIEW_UNTAG";
|
|
constexpr const char* kIdShowBoth = "CEREBELLUM_REASAMPLER_VIEW_SHOW_BOTH";
|
|
|
|
// The live session the actions mutate. Set once by designViewRegisterActions and
|
|
// read by the hookcommand handler. Not owned here (main.cpp owns g_session).
|
|
ReaSamplerSession* g_session = nullptr;
|
|
|
|
// Minted command ids (0 until registration succeeds). Compared in the handler.
|
|
int g_cmdToggleMode = 0;
|
|
int g_cmdActivateArrange = 0;
|
|
int g_cmdActivateDesign = 0;
|
|
int g_cmdTagDesign = 0;
|
|
int g_cmdTagArrange = 0;
|
|
int g_cmdUntag = 0;
|
|
int g_cmdShowBoth = 0;
|
|
|
|
// gaccel storage must outlive registration — REAPER holds each pointer until we
|
|
// mirror-unregister it. One per action.
|
|
gaccel_register_t g_accelToggleMode{};
|
|
gaccel_register_t g_accelActivateArrange{};
|
|
gaccel_register_t g_accelActivateDesign{};
|
|
gaccel_register_t g_accelTagDesign{};
|
|
gaccel_register_t g_accelTagArrange{};
|
|
gaccel_register_t g_accelUntag{};
|
|
gaccel_register_t g_accelShowBoth{};
|
|
|
|
// Mints a command id from a stable string and registers its gaccel (Actions-list
|
|
// entry with `desc`). Returns the command id (0 on failure). The gaccel storage is
|
|
// caller-owned and must outlive the module (the file-scope g_accel* above).
|
|
int registerAction(reaper_plugin_info_t* rec, const char* stableId,
|
|
gaccel_register_t& accel, const char* desc) {
|
|
const int cmd = rec->Register("command_id", (void*)stableId);
|
|
if (cmd) {
|
|
accel.accel.cmd = cmd;
|
|
accel.desc = desc;
|
|
rec->Register("gaccel", (void*)&accel);
|
|
}
|
|
return cmd;
|
|
}
|
|
|
|
// Collects the canonical GUID keys of the current track selection. Empty if nothing
|
|
// is selected. CountSelectedTracks/GetSelectedTrack ignore the master (SDK), which is
|
|
// exactly right — the master is never a tagged leaf.
|
|
std::vector<std::string> selectedTrackGuids() {
|
|
std::vector<std::string> guids;
|
|
const int n = CountSelectedTracks(nullptr); // nullptr = active project
|
|
guids.reserve(static_cast<std::size_t>(n < 0 ? 0 : n));
|
|
for (int i = 0; i < n; ++i) {
|
|
MediaTrack* tr = GetSelectedTrack(nullptr, i);
|
|
if (!tr) continue;
|
|
std::string g = guidString(tr);
|
|
if (!g.empty()) guids.push_back(std::move(g));
|
|
}
|
|
return guids;
|
|
}
|
|
|
|
// Reapplies the model's CURRENT active mode to the active project so a membership
|
|
// mutation takes visible effect immediately (park/unpark/re-derive parents). Called
|
|
// after every tag/untag/show-both. `proj = nullptr` -> REAPER's active project.
|
|
void reapplyActiveMode() {
|
|
applyMode(g_session->view(), g_session->view().activeModeId(), nullptr);
|
|
}
|
|
|
|
// Persists both the bank and the Design-View model to the active project's ext
|
|
// state. Called after every state-changing Design View action so the view model
|
|
// is not lost across save/close/reopen. Marking the project dirty is correct —
|
|
// a Design View mutation is a project-level change the user should be prompted
|
|
// to save.
|
|
//
|
|
// When the membership index is non-empty AND the project is unsaved, we prompt
|
|
// the user to Save-As before persisting — mirroring the flow capture uses.
|
|
// Gate: if membership is empty (no tracks tagged), skip the prompt entirely;
|
|
// saveToActiveProject will no-op for an unsaved project, which is correct.
|
|
//
|
|
// DAW-ONLY ASSUMPTION: Main_SaveProject(proj, true) opens a Save-As dialog and
|
|
// blocks until the user dismisses it. The blocking behaviour and dialog
|
|
// appearance can only be confirmed in a running REAPER (same caveat as capture).
|
|
void persistViewState() {
|
|
if (!g_session->view().membership().empty()) {
|
|
// At least one track is tagged — worth persisting. Check whether the
|
|
// project is saved and, if not, prompt Save-As so saveToActiveProject
|
|
// can write ext state. Mirrors capture's readRppPath idiom exactly.
|
|
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
|
if (proj) {
|
|
auto readRppPath = [&]() -> std::string {
|
|
std::vector<char> buf(4096, '\0');
|
|
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
|
return std::string(buf.data());
|
|
};
|
|
|
|
if (readRppPath().empty()) {
|
|
// Project is unsaved — prompt Save-As.
|
|
Main_SaveProject(proj, true);
|
|
// Re-read: still empty means the user cancelled.
|
|
if (readRppPath().empty()) {
|
|
ShowConsoleMsg(
|
|
"ReaSampler: Design View state will not persist until "
|
|
"the project is saved.\n");
|
|
// The in-session tag state is left as-is — the mode change
|
|
// already applied and remains valid for this session.
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
g_session->saveToActiveProject();
|
|
}
|
|
|
|
// -- Action bodies ---------------------------------------------------------
|
|
|
|
// Toggle: cycle to the next mode in ordinal order (Arrange <-> Design with two
|
|
// seeds; scales to cycle-through-all for >2 modes with no change here). applyMode
|
|
// itself sets the model's active mode, so we only compute the target and apply.
|
|
void doToggleMode() {
|
|
const std::string target =
|
|
nextModeId(g_session->view().modes(), g_session->view().activeModeId());
|
|
if (target.empty()) return; // no modes to cycle to (degenerate)
|
|
applyMode(g_session->view(), target, nullptr);
|
|
persistViewState();
|
|
}
|
|
|
|
// Direct jump to a named mode. applyMode is a no-op (returns false, no mutation) if
|
|
// the id is unregistered, so an absent mode fails safe.
|
|
void doActivateMode(const std::string& modeId) {
|
|
applyMode(g_session->view(), modeId, nullptr);
|
|
persistViewState();
|
|
}
|
|
|
|
// Tag the selection's leaves into `modeId`, then reapply so the change is immediate.
|
|
// tag() replaces any prior single-mode membership (a leaf lives in one mode; the
|
|
// cross-mode case is show-both), matching the D1 contract.
|
|
void doTag(const std::string& modeId) {
|
|
for (const std::string& g : selectedTrackGuids())
|
|
g_session->view().membership().tag(g, modeId);
|
|
reapplyActiveMode();
|
|
persistViewState();
|
|
}
|
|
|
|
// Untag the selection entirely (return each to the Arrange default). This is the
|
|
// shared body behind both "Untag selected" and "Tag -> Arrange" (Arrange = the
|
|
// absence of a tag), so the two actions are the same act by definition.
|
|
void doUntag() {
|
|
for (const std::string& g : selectedTrackGuids())
|
|
g_session->view().membership().untag(g);
|
|
reapplyActiveMode();
|
|
persistViewState();
|
|
}
|
|
|
|
// Toggle the per-track show-both pin for the selection. Read the CURRENT pin of each
|
|
// track and flip it independently (a mixed selection converges toward "all on" then
|
|
// "all off" only if uniform; per-track flip is the honest semantics of a toggle on a
|
|
// multi-selection). show-both leaves are never parked (D1), so reapply reflects the
|
|
// change immediately.
|
|
void doShowBoth() {
|
|
MembershipIndex& m = g_session->view().membership();
|
|
for (const std::string& g : selectedTrackGuids())
|
|
m.setShowBoth(g, !m.isShowBoth(g));
|
|
reapplyActiveMode();
|
|
persistViewState();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void designViewRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) {
|
|
g_session = session;
|
|
|
|
// command_id -> gaccel for each. The single hookcommand that routes these lives
|
|
// in main.cpp (one hook per extension); designViewHandleCommand services them.
|
|
g_cmdToggleMode = registerAction(rec, kIdToggleMode, g_accelToggleMode,
|
|
"ReaSampler: toggle Design View mode");
|
|
g_cmdActivateArrange = registerAction(rec, kIdActivateArrange, g_accelActivateArrange,
|
|
"ReaSampler: activate mode Arrange");
|
|
g_cmdActivateDesign = registerAction(rec, kIdActivateDesign, g_accelActivateDesign,
|
|
"ReaSampler: activate mode Design");
|
|
g_cmdTagDesign = registerAction(rec, kIdTagDesign, g_accelTagDesign,
|
|
"ReaSampler: tag selected tracks -> Design");
|
|
g_cmdTagArrange = registerAction(rec, kIdTagArrange, g_accelTagArrange,
|
|
"ReaSampler: tag selected tracks -> Arrange");
|
|
g_cmdUntag = registerAction(rec, kIdUntag, g_accelUntag,
|
|
"ReaSampler: untag selected tracks");
|
|
g_cmdShowBoth = registerAction(rec, kIdShowBoth, g_accelShowBoth,
|
|
"ReaSampler: show both for selected tracks");
|
|
}
|
|
|
|
bool designViewHandleCommand(int command) {
|
|
if (command == 0 || !g_session) return false;
|
|
|
|
if (command == g_cmdToggleMode) { doToggleMode(); return true; }
|
|
if (command == g_cmdActivateArrange) { doActivateMode(kArrangeModeId); return true; }
|
|
if (command == g_cmdActivateDesign) { doActivateMode(kDesignModeId); return true; }
|
|
if (command == g_cmdTagDesign) { doTag(kDesignModeId); return true; }
|
|
// Tag -> Arrange and Untag are the same act (Arrange = the absence of a tag).
|
|
if (command == g_cmdTagArrange) { doUntag(); return true; }
|
|
if (command == g_cmdUntag) { doUntag(); return true; }
|
|
if (command == g_cmdShowBoth) { doShowBoth(); return true; }
|
|
|
|
return false; // not ours — caller's hookcommand keeps looking
|
|
}
|
|
|
|
void designViewUnregisterActions(reaper_plugin_info_t* rec) {
|
|
// Mirror-unregister with '-'-prefixed strings, per the contract's unload rule.
|
|
// gaccel first, then the command_id string (reverse of registration order).
|
|
rec->Register("-gaccel", (void*)&g_accelShowBoth);
|
|
rec->Register("-command_id", (void*)kIdShowBoth);
|
|
rec->Register("-gaccel", (void*)&g_accelUntag);
|
|
rec->Register("-command_id", (void*)kIdUntag);
|
|
rec->Register("-gaccel", (void*)&g_accelTagArrange);
|
|
rec->Register("-command_id", (void*)kIdTagArrange);
|
|
rec->Register("-gaccel", (void*)&g_accelTagDesign);
|
|
rec->Register("-command_id", (void*)kIdTagDesign);
|
|
rec->Register("-gaccel", (void*)&g_accelActivateDesign);
|
|
rec->Register("-command_id", (void*)kIdActivateDesign);
|
|
rec->Register("-gaccel", (void*)&g_accelActivateArrange);
|
|
rec->Register("-command_id", (void*)kIdActivateArrange);
|
|
rec->Register("-gaccel", (void*)&g_accelToggleMode);
|
|
rec->Register("-command_id", (void*)kIdToggleMode);
|
|
|
|
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), NOT the Design-View path: a bank change is held in-session and written
|
|
// to the active project's ext state so it travels with the .rpp. Deliberately no
|
|
// Save-As prompt — saveToActiveProject no-ops on an unsaved project (the change stays
|
|
// valid for the session and persists on the user's next save), exactly as capture
|
|
// persists. This is an intentional divergence from persistViewState (above), which
|
|
// DOES prompt Save-As on an unsaved project; do not "align" the two — a bank mutation
|
|
// follows capture's quiet-persist idiom, a Design-View mutation follows the prompt idiom.
|
|
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.
|
|
//
|
|
// COMMA GUARD: GetUserInputs splits the returned values on a separator that defaults
|
|
// to ',', so a bank name containing a comma would be truncated at the comma. We
|
|
// override the return separator to \x1f (ASCII unit separator, un-typeable in the
|
|
// dialog) via the documented `separator=X` extra caption field (SDK ~3806), so any
|
|
// printable name — commas included — round-trips whole. The captions_csv itself stays
|
|
// comma-joined: the single field caption, then the `separator=` directive as a
|
|
// trailing pseudo-caption (the directive redefines only the RETURN separator).
|
|
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());
|
|
const std::string captions = std::string(caption) + ",separator=\x1f";
|
|
if (!GetUserInputs(title, 1, captions.c_str(), 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. 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. First-match is unambiguous BY
|
|
// CONSTRUCTION: the model enforces unique display names (trimmed + case-insensitive),
|
|
// so at most one bank can carry a given name — no duplicate can shadow another here.
|
|
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). The model rejects a display
|
|
// name that duplicates an existing bank's (trimmed + case-insensitive, incl. "Pool");
|
|
// the create then fails and the user is told the name is taken.
|
|
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 \"" + name +
|
|
"\" (a bank with that name already exists).\n")
|
|
.c_str());
|
|
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)) {
|
|
// renameBank rejects the pool (un-renamable) or a name already used by another
|
|
// bank (unique display names, trimmed + case-insensitive).
|
|
ShowConsoleMsg("ReaSampler: cannot rename that bank (the pool is un-renamable, "
|
|
"or another bank already uses that name).\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;
|
|
}
|
|
// Pool early-out: the pool is un-deletable (the model rejects it). Catch it here,
|
|
// BEFORE the non-empty confirm, so typing "Pool" never shows a misleading
|
|
// "delete anyway?" prompt 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 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 into a named destination bank (prompted
|
|
// by display name). The SOURCE is the bank the selection lives in — the focused
|
|
// region's displayed bank (bankPanelSelectedSourceBankId), which under B4's vertical
|
|
// split is NOT necessarily the active/capture-target bank (active ≠ shown). 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
|
|
// drives 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 bank the selection lives in (the focused region's displayed bank).
|
|
// Pass ids by value — no BankIndex& is cached across the loop's mutations.
|
|
const std::string srcId = bankPanelSelectedSourceBankId();
|
|
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
|