// 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 #include #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 selectedTrackGuids() { std::vector guids; const int n = CountSelectedTracks(nullptr); // nullptr = active project guids.reserve(static_cast(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 buf(4096, '\0'); EnumProjects(-1, buf.data(), static_cast(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 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(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 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 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