Q-W4: split actions.cpp into design_view_actions/bank_actions/prune_action + shared action_registry; bank verbs deduped into promptless bankOp* inner verbs in panel_bank_ops (one mutation home, two UX skins); command-id strings byte-identical; actions.h shim carrier retired
This commit is contained in:
+5
-2
@@ -365,7 +365,7 @@ target_include_directories(app_version PUBLIC src ${CMAKE_CURRENT_BINARY_DIR}/ge
|
|||||||
# FX-chain identity fold, and the pure parent-detection decision (resample-from-
|
# FX-chain identity fold, and the pure parent-detection decision (resample-from-
|
||||||
# sample by resolved file path). Split out so the encoding + decision logic are
|
# sample by resolved file path). Split out so the encoding + decision logic are
|
||||||
# unit-tested outside the DAW; the FX-chain query, capture re-run, and action
|
# unit-tested outside the DAW; the FX-chain query, capture re-run, and action
|
||||||
# registration stay in the shell (main.cpp / actions.cpp). No dependency on
|
# registration stay in the shell (main.cpp / shell/actions/). No dependency on
|
||||||
# bank_model — it takes plain strings/values at its boundary.
|
# bank_model — it takes plain strings/values at its boundary.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
add_library(provenance STATIC src/core/model/provenance.cpp)
|
add_library(provenance STATIC src/core/model/provenance.cpp)
|
||||||
@@ -1123,7 +1123,10 @@ add_library(reaper_reasampler MODULE
|
|||||||
src/core/view/guid_diff.cpp
|
src/core/view/guid_diff.cpp
|
||||||
src/core/view/lane_keys.cpp
|
src/core/view/lane_keys.cpp
|
||||||
src/shell/capture/item_read.cpp
|
src/shell/capture/item_read.cpp
|
||||||
src/actions.cpp
|
src/shell/actions/action_registry.cpp
|
||||||
|
src/shell/actions/design_view_actions.cpp
|
||||||
|
src/shell/actions/bank_actions.cpp
|
||||||
|
src/shell/actions/prune_action.cpp
|
||||||
src/ingest.cpp
|
src/ingest.cpp
|
||||||
src/core/model/bank_book.cpp
|
src/core/model/bank_book.cpp
|
||||||
src/core/model/owned_manifest.cpp
|
src/core/model/owned_manifest.cpp
|
||||||
|
|||||||
-1019
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "core/namespaces.h"
|
|
||||||
// actions — the Design View action family (Phase D4). Registers the bindable
|
|
||||||
// actions that drive the mode workflow and wires them end-to-end: toggle/activate
|
|
||||||
// a mode, tag/untag/show-both the current track selection. Each action mutates the
|
|
||||||
// session's ViewModeModel (D1, via persist's ReaSamplerSession) and then reapplies
|
|
||||||
// the active mode through the view shell (D2) so the change takes effect immediately.
|
|
||||||
//
|
|
||||||
// REAPER-facing shell: the .cpp includes reaper_plugin_functions.h WITHOUT
|
|
||||||
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers — CLAUDE.md §contract). This
|
|
||||||
// header is SDK-free; main.cpp calls register/handle/unregister and nothing else.
|
|
||||||
//
|
|
||||||
// Split out of main.cpp (rather than inlined there) to match CONTEXT.md's planned
|
|
||||||
// `actions` module and keep main.cpp's entrypoint focused on API-pointer ownership
|
|
||||||
// and lifecycle. The reapply-on-open glue stays in main.cpp (it owns the timer that
|
|
||||||
// drives persist.poll()); this module only registers and services the actions.
|
|
||||||
|
|
||||||
// Forward-declared at GLOBAL scope (matches reaper_plugin.h's typedef struct
|
|
||||||
// reaper_plugin_info_t) so this header stays SDK-free; the .cpp includes the real
|
|
||||||
// definition. Declared before the namespace so it is the global type, not a
|
|
||||||
// namespace-local shadow.
|
|
||||||
struct reaper_plugin_info_t;
|
|
||||||
|
|
||||||
namespace reasampler {
|
|
||||||
|
|
||||||
class ReaSamplerSession;
|
|
||||||
|
|
||||||
// Registers the Design View action family against `rec` (command_id + gaccel +
|
|
||||||
// hookcommand-routing is owned by the caller's single hookcommand). `session` is the
|
|
||||||
// live session the actions mutate; it must outlive the registration. Idempotent is
|
|
||||||
// NOT promised — call exactly once at load, mirror-unregister once at unload.
|
|
||||||
void designViewRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session);
|
|
||||||
|
|
||||||
// Services one fired command. Returns true iff `command` is one of this module's
|
|
||||||
// action ids (and it was handled); false otherwise so the caller's hookcommand keeps
|
|
||||||
// looking (per the contract: claim only our own ids). Safe to call for any command.
|
|
||||||
bool designViewHandleCommand(int command);
|
|
||||||
|
|
||||||
// Mirror-unregisters everything designViewRegisterActions registered, with the
|
|
||||||
// '-'-prefixed strings (per the contract's unload rule). Call once on rec==nullptr.
|
|
||||||
void designViewUnregisterActions(reaper_plugin_info_t* rec);
|
|
||||||
|
|
||||||
// --- Multi-bank action family (Phase B3) -----------------------------------
|
|
||||||
// The bindable action set that drives the multi-bank workflow: create / rename /
|
|
||||||
// delete / evacuate a bank, activate a bank (direct pool/design-free + cycle), move /
|
|
||||||
// copy the panel's selected samples into a bank, and the two vertical-split
|
|
||||||
// full-height toggles. Every mutating action drives the B1 model on
|
|
||||||
// g_session.book() and persists via g_session.saveToActiveProject() so the change
|
|
||||||
// travels with the .rpp; the toggles flip the B4-rendered layout bit on the panel.
|
|
||||||
//
|
|
||||||
// Same registration/routing/unload contract as the Design View family above and the
|
|
||||||
// same shared g_session. Kept a distinct trio (not folded into the Design View one)
|
|
||||||
// because the two families are orthogonal pillars — but they share the single
|
|
||||||
// hookcommand main.cpp owns; each family's Handle claims only its own ids.
|
|
||||||
|
|
||||||
// Registers the multi-bank family against `rec`. `session` is the live session (must
|
|
||||||
// outlive registration). Call exactly once at load. Shares g_session with the Design
|
|
||||||
// View family — pass the SAME session pointer.
|
|
||||||
void bankRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session);
|
|
||||||
|
|
||||||
// Services one fired command for the multi-bank family. True iff it was one of this
|
|
||||||
// family's ids (and handled); false otherwise so the caller's hookcommand keeps
|
|
||||||
// looking. Safe for any command.
|
|
||||||
bool bankHandleCommand(int command);
|
|
||||||
|
|
||||||
// Mirror-unregisters the multi-bank family with '-'-prefixed strings. Call once on
|
|
||||||
// rec==nullptr (before g_session is torn down).
|
|
||||||
void bankUnregisterActions(reaper_plugin_info_t* rec);
|
|
||||||
|
|
||||||
// The registered command id for the "Prune bank folder" action (Phase R, R3), or 0 before
|
|
||||||
// registration. The bank_panel prune button fires the action THROUGH this id via
|
|
||||||
// Main_OnCommand (fork R-E: the button dispatches the command, it does not call the session
|
|
||||||
// directly) so the panel affordance and the bindable action share one guarded code path.
|
|
||||||
int bankPruneCommandId();
|
|
||||||
|
|
||||||
// Persists a completed bank-index verb as a single REAPER undo point (R-B).
|
|
||||||
// Wraps persistBook() (= SetProjExtState) in a Begin/End block with UNDO_STATE_MISCCFG
|
|
||||||
// so the bank op is one Ctrl-Z. On an unsaved / no-active project persistBook() no-ops
|
|
||||||
// and the block is closed with an empty label + zero flag (REAPER discards it). Callers
|
|
||||||
// must invoke this ONLY after a successful/effective mutation — rejected ops (duplicate
|
|
||||||
// name, un-deletable pool, etc.) must return before reaching here so no empty undo
|
|
||||||
// point is ever opened for a no-op. Defined in actions.cpp alongside persistBook().
|
|
||||||
//
|
|
||||||
// S9 bank-generation bump: pass `bumpGeneration = true` for a verb that changes what a live
|
|
||||||
// instance would PLAY — move / copy / remove / evacuate / delete-with-members (a sample left,
|
|
||||||
// arrived, or dropped out of a bank an instance may reference). Leave it false (the default)
|
|
||||||
// for a PURELY ORGANIZATIONAL verb — create / rename / activate / reorder — which changes no
|
|
||||||
// existing (bankId, sampleId) -> content mapping, so no instance need refresh. The bump (when
|
|
||||||
// requested) happens INSIDE the block, BEFORE persistBook(), so the stamped counter rides the
|
|
||||||
// same ext-state write and undo captures the pre/post generation with the rest of the blob.
|
|
||||||
void persistBankOp(const char* label, bool bumpGeneration = false);
|
|
||||||
|
|
||||||
} // namespace reasampler
|
|
||||||
+2
-1
@@ -28,11 +28,12 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "actions.h"
|
|
||||||
#include "core/capture/render_settings.h" // captureActionTable
|
#include "core/capture/render_settings.h" // captureActionTable
|
||||||
#include "core/version/app_version.h" // channelCommandId / channelActionName / appVersion
|
#include "core/version/app_version.h" // channelCommandId / channelActionName / appVersion
|
||||||
#include "ingest.h"
|
#include "ingest.h"
|
||||||
#include "persist.h"
|
#include "persist.h"
|
||||||
|
#include "shell/actions/bank_actions.h" // multi-bank action family (B3; Q-W4 home)
|
||||||
|
#include "shell/actions/design_view_actions.h" // Design View action family (D4; Q-W4 home)
|
||||||
#include "shell/capture/capture_batch.h" // batch + recapture action bodies
|
#include "shell/capture/capture_batch.h" // batch + recapture action bodies
|
||||||
#include "shell/capture/capture_orchestrator.h" // single-capture / realtime / insert action bodies
|
#include "shell/capture/capture_orchestrator.h" // single-capture / realtime / insert action bodies
|
||||||
#include "shell/capture/realtime_lifecycle.h" // in-flight realtime state + tick driver
|
#include "shell/capture/realtime_lifecycle.h" // in-flight realtime state + tick driver
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// provenance — the REAPER-free core behind Milestone 10 (re-capture from source).
|
// provenance — the REAPER-free core behind Milestone 10 (re-capture from source).
|
||||||
//
|
//
|
||||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
||||||
// vendor/ includes. Standard library only. The shell (main.cpp / actions.cpp)
|
// vendor/ includes. Standard library only. The shell (main.cpp / the action families)
|
||||||
// gathers the raw inputs from REAPER — the source item media-file names, the
|
// gathers the raw inputs from REAPER — the source item media-file names, the
|
||||||
// source track FX-chain identity (names / GUIDs / enabled flags), the exact
|
// source track FX-chain identity (names / GUIDs / enabled flags), the exact
|
||||||
// capture range, scope, tail — and hands plain strings/values here. This module
|
// capture range, scope, tail — and hands plain strings/values here. This module
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ const std::string& vstPluginName();
|
|||||||
|
|
||||||
// --- Channel-qualified action id / name builders ------------------------------------
|
// --- Channel-qualified action id / name builders ------------------------------------
|
||||||
//
|
//
|
||||||
// The two composition helpers every action-registering shell (main.cpp, actions.cpp)
|
// The two composition helpers every action-registering shell (main.cpp, action_registry)
|
||||||
// funnels through, so command ids and Actions-list names are qualified IDENTICALLY on
|
// funnels through, so command ids and Actions-list names are qualified IDENTICALLY on
|
||||||
// every channel from ONE definition — no shell re-implements the concatenation.
|
// every channel from ONE definition — no shell re-implements the concatenation.
|
||||||
//
|
//
|
||||||
|
|||||||
+2
-3
@@ -16,7 +16,6 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "actions.h" // persistBankOp — shared undo-block wrapper (R-B path)
|
|
||||||
#include "core/version/app_version.h" // channelCommandId / channelActionName
|
#include "core/version/app_version.h" // channelCommandId / channelActionName
|
||||||
#include "core/wire/assignment_request.h" // pure (bankId, sampleId, generation) encode
|
#include "core/wire/assignment_request.h" // pure (bankId, sampleId, generation) encode
|
||||||
#include "core/model/bank_book.h" // BankBook, Bank, activeBankId / activeIndex
|
#include "core/model/bank_book.h" // BankBook, Bank, activeBankId / activeIndex
|
||||||
@@ -435,7 +434,7 @@ void doImportFromMediaExplorer() {
|
|||||||
// The generation is bumped only when something NEW landed (a dedup collapse mutated nothing,
|
// The generation is bumped only when something NEW landed (a dedup collapse mutated nothing,
|
||||||
// so it needs neither a bump nor a persist to resolve — the sample is already in ext-state).
|
// so it needs neither a bump nor a persist to resolve — the sample is already in ext-state).
|
||||||
// If saveToActiveProject() no-ops (unsaved project), close with an empty label + zero flag so
|
// If saveToActiveProject() no-ops (unsaved project), close with an empty label + zero flag so
|
||||||
// REAPER discards the undo entry (the house pattern from actions.cpp). importFileIntoActiveBank
|
// REAPER discards the undo entry (the house pattern from persistBankOp). importFileIntoActiveBank
|
||||||
// already refuses on an unsaved project, so in practice the persist here succeeds.
|
// already refuses on an unsaved project, so in practice the persist here succeeds.
|
||||||
Undo_BeginBlock2(nullptr);
|
Undo_BeginBlock2(nullptr);
|
||||||
bool persisted = true; // true when nothing needed persisting (dedup) — governs the label path
|
bool persisted = true; // true when nothing needed persisting (dedup) — governs the label path
|
||||||
@@ -508,7 +507,7 @@ void ingestDroppedFiles(const std::vector<std::string>& absolutePaths) {
|
|||||||
// One undo point for the whole drop, opened only if a NEW index entry was created (a
|
// One undo point for the whole drop, opened only if a NEW index entry was created (a
|
||||||
// drop that only re-hit existing content mutated nothing on the index).
|
// drop that only re-hit existing content mutated nothing on the index).
|
||||||
// If saveToActiveProject() no-ops (unsaved project), we close with an empty label + zero
|
// If saveToActiveProject() no-ops (unsaved project), we close with an empty label + zero
|
||||||
// flag so REAPER discards the undo entry (house pattern from actions.cpp).
|
// flag so REAPER discards the undo entry (house pattern from persistBankOp).
|
||||||
if (importedNew > 0) {
|
if (importedNew > 0) {
|
||||||
Undo_BeginBlock2(nullptr);
|
Undo_BeginBlock2(nullptr);
|
||||||
// S9: one coalesced generation bump for the whole drop (>=1 new sample landed) so
|
// S9: one coalesced generation bump for the whole drop (>=1 new sample landed) so
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// action_registry.cpp — shared registration plumbing (Q-W4 split of actions.cpp).
|
||||||
|
// See action_registry.h. Needs no REAPER API pointers: rec->Register is a member
|
||||||
|
// call on the dispatch struct REAPER hands the entry point.
|
||||||
|
|
||||||
|
#include "shell/actions/action_registry.h"
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/version/app_version.h" // channelCommandId / channelActionName
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using version::channelActionName;
|
||||||
|
using version::channelCommandId;
|
||||||
|
|
||||||
|
// Durable store of composed, channel-qualified strings (ids + labels). A std::deque
|
||||||
|
// never invalidates references on push_back, so a c_str() handed to REAPER (a
|
||||||
|
// command_id at register, a gaccel desc for its lifetime) stays valid until process
|
||||||
|
// exit. Memoized by suffix so register and the mirror-unregister get the SAME id
|
||||||
|
// pointer for a given action.
|
||||||
|
std::deque<std::string> g_strStore;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const char* channelIdFor(const char* suffix) {
|
||||||
|
const std::string composed = channelCommandId(suffix);
|
||||||
|
for (const std::string& s : g_strStore)
|
||||||
|
if (s == composed) return s.c_str();
|
||||||
|
g_strStore.push_back(composed);
|
||||||
|
return g_strStore.back().c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
int registerAction(reaper_plugin_info_t* rec, const char* suffix,
|
||||||
|
gaccel_register_t& accel, const char* phrase) {
|
||||||
|
const char* id = channelIdFor(suffix);
|
||||||
|
const int cmd = rec->Register("command_id", (void*)id);
|
||||||
|
if (cmd) {
|
||||||
|
g_strStore.push_back(channelActionName(phrase));
|
||||||
|
accel.accel.cmd = cmd;
|
||||||
|
accel.desc = g_strStore.back().c_str();
|
||||||
|
rec->Register("gaccel", (void*)&accel);
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
// action_registry — shared registration plumbing for the bindable action families
|
||||||
|
// (Q-W4 split of actions.cpp). Owns the durable interned-string store both the
|
||||||
|
// Design View and multi-bank families register through, so a composed command id
|
||||||
|
// keeps ONE stable pointer from register to the mirror-unregister, and the
|
||||||
|
// register-a-command_id-then-gaccel sequence has one implementation.
|
||||||
|
//
|
||||||
|
// Includes reaper_plugin.h (gaccel_register_t / reaper_plugin_info_t full defs);
|
||||||
|
// only the action-family TUs include this header. Q-W6's registration table
|
||||||
|
// subsumes this helper when the hand-written blocks become data.
|
||||||
|
|
||||||
|
#include "reaper_plugin.h" // reaper_plugin_info_t, gaccel_register_t
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
// Returns the channel-qualified command id for `suffix`, interning it once for the
|
||||||
|
// process lifetime. Called by BOTH registerAction and each family's unregister path,
|
||||||
|
// so a '-command_id' presents the IDENTICAL string pointer registered earlier.
|
||||||
|
const char* channelIdFor(const char* suffix);
|
||||||
|
|
||||||
|
// Mints a command id from a channel-qualified SUFFIX and registers its gaccel
|
||||||
|
// (Actions-list entry with a channel-qualified label PHRASE). Returns the command id
|
||||||
|
// (0 on failure). Both the composed id and label are interned durably — REAPER holds
|
||||||
|
// the desc pointer, and the id must survive to the mirror-unregister. The gaccel
|
||||||
|
// storage itself is caller-owned (file-scope in the family TU).
|
||||||
|
int registerAction(reaper_plugin_info_t* rec, const char* suffix,
|
||||||
|
gaccel_register_t& accel, const char* phrase);
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
// bank_actions.cpp — the multi-bank bindable action family (Phase B3; Q-W4 split of
|
||||||
|
// actions.cpp). See bank_actions.h.
|
||||||
|
//
|
||||||
|
// Q-W4 dedupe: each mutating handler is a THIN UX SKIN — text prompts (promptText),
|
||||||
|
// name resolution, and console feedback — over the promptless bankOp* inner verbs
|
||||||
|
// homed in panel_bank_ops (model op + persistBankOp, one bank op = one Ctrl-Z). The
|
||||||
|
// book's rules (pool privileges, collapse-by-hash, active-fallback-to-pool) all live
|
||||||
|
// in bank_book; these handlers only drive the verbs and react to the boolean.
|
||||||
|
//
|
||||||
|
// 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 BankModel& (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 the verbs.
|
||||||
|
//
|
||||||
|
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||||
|
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers (CLAUDE.md §contract).
|
||||||
|
// The action ids are minted from FOREVER-STABLE strings; user keybindings key off
|
||||||
|
// them, so they must never change 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 "persist.h" // ReaSamplerSession (owns book())
|
||||||
|
#include "shell/panel/panel_bank_ops.h" // bankOp* inner verbs + promptText + selection seam
|
||||||
|
#include "shell/panel/panel_layout.h" // full-height toggles (B3)
|
||||||
|
|
||||||
|
#define REAPERAPI_MINIMAL
|
||||||
|
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||||
|
#define REAPERAPI_WANT_ShowMessageBox
|
||||||
|
#include "reaper_plugin_functions.h"
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// FOREVER-STABLE multi-bank action-id SUFFIXES (Phase V, V4). The channel family prefix is
|
||||||
|
// prepended at register via channelCommandId (as with the Design View family) — stable
|
||||||
|
// rebuilds the shipped id, beta the isolated one. NEVER change a shipped suffix.
|
||||||
|
// Each suffix + the stable prefix must byte-match the pre-V4 shipped literal exactly
|
||||||
|
// (e.g. "BANK_REMOVE_SELECTED" -> "CEREBELLUM_REASAMPLER_BANK_REMOVE_SELECTED").
|
||||||
|
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";
|
||||||
|
// Phase R (Reclaim), R2: the FOREVER-STABLE "Prune bank folder" id. Registered NOW so
|
||||||
|
// in-DAW dry-run verification is possible; R2 behaviour is REPORT-ONLY (no deletion),
|
||||||
|
// and R3 extends the confirm-and-delete step behind this SAME id — never a throwaway id.
|
||||||
|
constexpr const char* kIdBankPruneFolder = "BANK_PRUNE_FOLDER";
|
||||||
|
|
||||||
|
// The live session the actions read (name resolution, member counts, prune). The
|
||||||
|
// mutations themselves run through the bankOp* verbs, which resolve the same session
|
||||||
|
// via the panel seam. Set once by bankRegisterActions; not owned here.
|
||||||
|
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 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 (thin UX skins over the bankOp* verbs) -------------------
|
||||||
|
|
||||||
|
// Create a named bank: prompt for a display name; the verb mints a stable GUID id,
|
||||||
|
// creates it in the model, persists. The new bank is NOT auto-activated (create and
|
||||||
|
// activate are distinct acts — mirrors capture/placement separation). The model
|
||||||
|
// rejects a duplicate display name (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;
|
||||||
|
if (bankOpCreate(name).empty()) {
|
||||||
|
ShowConsoleMsg(
|
||||||
|
("ReaSampler: could not create bank \"" + name +
|
||||||
|
"\" (a bank with that name already exists).\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; the 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 (!bankOpRename(id, newName)) {
|
||||||
|
// The verb 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) lives in the panel.
|
||||||
|
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 the delete; 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)
|
||||||
|
}
|
||||||
|
// S9: bump only when the deleted bank held samples — dropping them changes what a live
|
||||||
|
// instance referencing one could play. Deleting an EMPTY bank is purely organizational.
|
||||||
|
if (!bankOpDelete(id, /*bumpGeneration=*/members > 0)) {
|
||||||
|
ShowConsoleMsg("ReaSampler: cannot delete that bank (the pool is un-deletable).\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 verb 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 (!bankOpEvacuate(id)) {
|
||||||
|
ShowConsoleMsg("ReaSampler: cannot evacuate that bank (the pool is the "
|
||||||
|
"destination, not a source).\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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. The verb persists 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)
|
||||||
|
bankOpActivate(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 panel affordance.
|
||||||
|
void doBankActivatePool() {
|
||||||
|
bankOpActivate(kPoolBankId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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); the verb owns the verb-aware no-op guardrail and
|
||||||
|
// destination collapse-by-hash. The panel's "move to bank" menu drives the same verb
|
||||||
|
// 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).
|
||||||
|
const std::string srcId = bankPanelSelectedSourceBankId();
|
||||||
|
if (srcId == destId) {
|
||||||
|
ShowConsoleMsg("ReaSampler: source and destination are the same bank.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bankOpTransfer(selected, srcId, destId, copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the panel's selected samples from the SOURCE bank (the focused region's
|
||||||
|
// displayed bank — same source as move/copy). Index-only and non-destructive to the
|
||||||
|
// file (orphaned until Phase R prune); silent, with the batched undo as recovery —
|
||||||
|
// see bankOpRemove for the full contract.
|
||||||
|
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(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");
|
||||||
|
// Phase R, R2: the "Prune bank folder" action (report-only in this wave; R3 extends
|
||||||
|
// the confirm-and-delete step behind this SAME forever-stable id).
|
||||||
|
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) {
|
||||||
|
// Mirror-unregister with '-'-prefixed strings, reverse of registration order. Each
|
||||||
|
// '-command_id' re-presents the same interned channel-qualified 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
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
// bank_actions — the multi-bank bindable action family (Phase B3; Q-W4 split of
|
||||||
|
// actions.h). The bindable action set that drives the multi-bank workflow: create /
|
||||||
|
// rename / delete / evacuate a bank, activate a bank (direct pool + cycle), move /
|
||||||
|
// copy / remove the panel's selected samples, the two vertical-split full-height
|
||||||
|
// toggles, and the Phase R prune action's registration + dispatch (its guarded body
|
||||||
|
// lives in prune_action). Q-W4 dedupe: every mutating handler here is a THIN UX skin
|
||||||
|
// (text prompts + console messages) over the promptless bankOp* verbs homed in
|
||||||
|
// panel_bank_ops — one implementation home for each mutation, two UX skins (this
|
||||||
|
// family prompts for which bank; the panel acts on a clicked tab).
|
||||||
|
//
|
||||||
|
// Same registration/routing/unload contract as the Design View family
|
||||||
|
// (design_view_actions); both share main.cpp's single hookcommand, and each family's
|
||||||
|
// Handle claims only its own ids. This header is SDK-free.
|
||||||
|
|
||||||
|
// Forward-declared at GLOBAL scope (matches reaper_plugin.h's typedef) so this
|
||||||
|
// header stays SDK-free; the .cpp includes the real definition.
|
||||||
|
struct reaper_plugin_info_t;
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
class ReaSamplerSession;
|
||||||
|
|
||||||
|
// Registers the multi-bank family against `rec`. `session` is the live session (must
|
||||||
|
// outlive registration). Call exactly once at load — pass the SAME session pointer
|
||||||
|
// the Design View family receives.
|
||||||
|
void bankRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session);
|
||||||
|
|
||||||
|
// Services one fired command for the multi-bank family. True iff it was one of this
|
||||||
|
// family's ids (and handled); false otherwise so the caller's hookcommand keeps
|
||||||
|
// looking. Safe for any command.
|
||||||
|
bool bankHandleCommand(int command);
|
||||||
|
|
||||||
|
// Mirror-unregisters the multi-bank family with '-'-prefixed strings. Call once on
|
||||||
|
// rec==nullptr (before the session is torn down).
|
||||||
|
void bankUnregisterActions(reaper_plugin_info_t* rec);
|
||||||
|
|
||||||
|
// The registered command id for the "Prune bank folder" action (Phase R, R3), or 0
|
||||||
|
// before registration. The bank_panel prune button fires the action THROUGH this id
|
||||||
|
// via Main_OnCommand (fork R-E: the button dispatches the command, it does not call
|
||||||
|
// the session directly) so the panel affordance and the bindable action share one
|
||||||
|
// guarded code path.
|
||||||
|
int bankPruneCommandId();
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
// design_view_actions.cpp — the Design View action family (Phase D4; Q-W4 split of
|
||||||
|
// actions.cpp). See design_view_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 "shell/actions/design_view_actions.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "shell/actions/action_registry.h" // channelIdFor / registerAction (shared plumbing)
|
||||||
|
|
||||||
|
#include "core/view/lane_keys.h" // view::isOnManualLane — the single managed/manual predicate
|
||||||
|
#include "core/view/view_mode_model.h"
|
||||||
|
#include "persist.h" // ReaSamplerSession (owns view() model)
|
||||||
|
#include "shell/capture/item_read.h" // shared MediaItem* -> GUID + fixed-lane-name reads (D2 W3-B)
|
||||||
|
#include "shell/capture/track_guid.h" // shared MediaTrack* -> canonical GUID key
|
||||||
|
#include "shell/panel/panel_window.h" // bankPanelInvalidate — footer toggle repaint
|
||||||
|
#include "shell/view/view.h" // applyMode + mintManagedLanes (D2 shell)
|
||||||
|
|
||||||
|
#define REAPERAPI_MINIMAL
|
||||||
|
#define REAPERAPI_WANT_CountSelectedTracks
|
||||||
|
#define REAPERAPI_WANT_GetSelectedTrack
|
||||||
|
#define REAPERAPI_WANT_CountSelectedMediaItems
|
||||||
|
#define REAPERAPI_WANT_GetSelectedMediaItem
|
||||||
|
#define REAPERAPI_WANT_GetMediaItemTrack
|
||||||
|
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||||
|
#define REAPERAPI_WANT_EnumProjects
|
||||||
|
#define REAPERAPI_WANT_Main_SaveProject
|
||||||
|
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||||
|
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||||
|
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||||
|
#include "reaper_plugin_functions.h"
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
using view::isOnManualLane;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// FOREVER-STABLE action-id SUFFIXES (Phase V, V4). The channel family prefix is prepended
|
||||||
|
// at register time via channelCommandId (app_version), so stable rebuilds the exact shipped
|
||||||
|
// id ("CEREBELLUM_REASAMPLER_VIEW_TOGGLE_MODE") and beta yields the isolated forever-family
|
||||||
|
// id ("CEREBELLUM_REASAMPLER_BETA_VIEW_TOGGLE_MODE"). Each composed id is minted into a
|
||||||
|
// persistent command id user keybindings key off — NEVER change a shipped suffix after ship.
|
||||||
|
constexpr const char* kIdToggleMode = "VIEW_TOGGLE_MODE";
|
||||||
|
constexpr const char* kIdActivateArrange = "VIEW_ACTIVATE_ARRANGE";
|
||||||
|
constexpr const char* kIdActivateDesign = "VIEW_ACTIVATE_DESIGN";
|
||||||
|
constexpr const char* kIdTagDesign = "VIEW_TAG_DESIGN";
|
||||||
|
constexpr const char* kIdTagArrange = "VIEW_TAG_ARRANGE";
|
||||||
|
constexpr const char* kIdUntag = "VIEW_UNTAG";
|
||||||
|
constexpr const char* kIdShowBoth = "VIEW_SHOW_BOTH";
|
||||||
|
// D2 Wave 3-B item-level mode moves — the item analog of the track tag family. Same
|
||||||
|
// FOREVER-STABLE contract (suffix composed with the channel prefix) — NEVER change these.
|
||||||
|
constexpr const char* kIdMoveItemsDesign = "VIEW_MOVE_ITEMS_DESIGN";
|
||||||
|
constexpr const char* kIdMoveItemsArrange = "VIEW_MOVE_ITEMS_ARRANGE";
|
||||||
|
constexpr const char* kIdUntagItems = "VIEW_UNTAG_ITEMS";
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
int g_cmdMoveItemsDesign = 0;
|
||||||
|
int g_cmdMoveItemsArrange = 0;
|
||||||
|
int g_cmdUntagItems = 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{};
|
||||||
|
gaccel_register_t g_accelMoveItemsDesign{};
|
||||||
|
gaccel_register_t g_accelMoveItemsArrange{};
|
||||||
|
gaccel_register_t g_accelUntagItems{};
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track fixed-lane mode value (I_FREEMODE=2). Mirrors the shell's constant; used only to
|
||||||
|
// decide whether an item's lane name is meaningful for the manual-lane read.
|
||||||
|
constexpr int kFreeModeFixedLanes = 2;
|
||||||
|
|
||||||
|
// Collects the current media-item selection as the pure decision's input: each selected
|
||||||
|
// item's GUID plus whether it sits on a MANUAL lane (⇒ EXEMPT — never retagged/re-laned).
|
||||||
|
// The manual-lane read follows the shared pure predicate exactly as the shell's readers
|
||||||
|
// do: only on a fixed-lane track (I_FREEMODE==2) is the item's lane name read; on a normal
|
||||||
|
// track isOnManualLane returns false for the empty name, so the P_LANENAME read is skipped.
|
||||||
|
// Items whose GUID cannot be read are dropped (an empty GUID must never be retagged).
|
||||||
|
std::vector<RetagItem> selectedRetagItems() {
|
||||||
|
std::vector<RetagItem> items;
|
||||||
|
const int n = CountSelectedMediaItems(nullptr); // nullptr = active project
|
||||||
|
items.reserve(static_cast<std::size_t>(n < 0 ? 0 : n));
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
MediaItem* it = GetSelectedMediaItem(nullptr, i);
|
||||||
|
if (!it) continue;
|
||||||
|
std::string g = itemGuid(it);
|
||||||
|
if (g.empty()) continue;
|
||||||
|
|
||||||
|
MediaTrack* tr = GetMediaItemTrack(it);
|
||||||
|
const bool fixedLane =
|
||||||
|
tr && static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes;
|
||||||
|
// Only read the lane name on a fixed-lane track; the pure predicate handles the
|
||||||
|
// normal-track case (returns false) so we pass an empty name and skip the read.
|
||||||
|
const std::string laneNm = fixedLane ? itemLaneName(tr, it) : std::string{};
|
||||||
|
items.push_back(RetagItem{std::move(g), isOnManualLane(fixedLane, laneNm)});
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
bankPanelInvalidate(); // repaint the footer [Arrange|Design] toggle immediately
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
bankPanelInvalidate(); // repaint the footer [Arrange|Design] toggle immediately
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Item-level mode moves (D2 Wave 3-B) -----------------------------------
|
||||||
|
//
|
||||||
|
// Retag the current ITEM selection to `targetMode` (empty ⇒ untag → Arrange default),
|
||||||
|
// then re-drive the minting + apply path so each moved item lands on its target mode's
|
||||||
|
// managed lane and the active-mode lane visibility is reasserted. The pure planItemRetag
|
||||||
|
// decides which selected items to retag (manual-lane items are EXEMPT — never retagged,
|
||||||
|
// never re-laned), upholding the managed-lanes-only invariant even under this explicit
|
||||||
|
// user action. The whole structural act is wrapped in ONE Undo block with a descriptive
|
||||||
|
// label (the inner blocks mintManagedLanes / applyMode open nest harmlessly under it).
|
||||||
|
//
|
||||||
|
// UNDO/SAVE ordering: persistViewState may pop a Save-As dialog (Main_SaveProject) which
|
||||||
|
// must NOT sit inside the Undo block, so we close the block first, then persist — the same
|
||||||
|
// separation the track actions rely on (they persist outside applyMode's own block).
|
||||||
|
void doMoveItems(const std::string& targetMode) {
|
||||||
|
const std::vector<RetagItem> selected = selectedRetagItems();
|
||||||
|
const std::vector<ItemRetagOp> ops = planItemRetag(selected, targetMode);
|
||||||
|
if (ops.empty()) return; // nothing selected, or every selected item was exempt/empty
|
||||||
|
|
||||||
|
MembershipIndex& membership = g_session->view().membership();
|
||||||
|
|
||||||
|
Undo_BeginBlock2(nullptr);
|
||||||
|
// Apply the pure decision's membership writes: tag into targetMode, or untag.
|
||||||
|
for (const ItemRetagOp& op : ops) {
|
||||||
|
if (op.untag) membership.untag(op.guid);
|
||||||
|
else membership.tag(op.guid, op.modeId);
|
||||||
|
}
|
||||||
|
// Re-drive the SAME minting/apply path auto-tag uses: mint/split lanes for any track
|
||||||
|
// whose items now span modes and assign each moved item to its mode's managed lane,
|
||||||
|
// then reassert the active mode's lane visibility. Manual lanes stay untouched
|
||||||
|
// (mintManagedLanes reports their items exempt and never mints over them).
|
||||||
|
mintManagedLanes(g_session->view(), nullptr);
|
||||||
|
reapplyActiveMode();
|
||||||
|
|
||||||
|
const std::string label =
|
||||||
|
targetMode.empty()
|
||||||
|
? std::string("ReaSampler: untag selected items")
|
||||||
|
: std::string("ReaSampler: move selected items -> ") + targetMode;
|
||||||
|
Undo_EndBlock2(nullptr, label.c_str(), -1);
|
||||||
|
|
||||||
|
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,
|
||||||
|
"toggle Design View mode");
|
||||||
|
g_cmdActivateArrange = registerAction(rec, kIdActivateArrange, g_accelActivateArrange,
|
||||||
|
"activate mode Arrange");
|
||||||
|
g_cmdActivateDesign = registerAction(rec, kIdActivateDesign, g_accelActivateDesign,
|
||||||
|
"activate mode Design");
|
||||||
|
g_cmdTagDesign = registerAction(rec, kIdTagDesign, g_accelTagDesign,
|
||||||
|
"tag selected tracks -> Design");
|
||||||
|
g_cmdTagArrange = registerAction(rec, kIdTagArrange, g_accelTagArrange,
|
||||||
|
"tag selected tracks -> Arrange");
|
||||||
|
g_cmdUntag = registerAction(rec, kIdUntag, g_accelUntag,
|
||||||
|
"untag selected tracks");
|
||||||
|
g_cmdShowBoth = registerAction(rec, kIdShowBoth, g_accelShowBoth,
|
||||||
|
"show both for selected tracks");
|
||||||
|
|
||||||
|
// Item-level mode moves (D2 W3-B): the item analog of the track tag family.
|
||||||
|
g_cmdMoveItemsDesign = registerAction(rec, kIdMoveItemsDesign, g_accelMoveItemsDesign,
|
||||||
|
"move selected items -> Design");
|
||||||
|
g_cmdMoveItemsArrange = registerAction(rec, kIdMoveItemsArrange, g_accelMoveItemsArrange,
|
||||||
|
"move selected items -> Arrange");
|
||||||
|
g_cmdUntagItems = registerAction(rec, kIdUntagItems, g_accelUntagItems,
|
||||||
|
"untag selected items");
|
||||||
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
// Item-level moves. Move -> Arrange and Untag items collapse to the same act (an
|
||||||
|
// empty target ⇒ untag ⇒ Arrange default), mirroring the track-level pairing above.
|
||||||
|
if (command == g_cmdMoveItemsDesign) { doMoveItems(kDesignModeId); return true; }
|
||||||
|
if (command == g_cmdMoveItemsArrange) { doMoveItems(std::string{}); return true; }
|
||||||
|
if (command == g_cmdUntagItems) { doMoveItems(std::string{}); 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 — the item
|
||||||
|
// moves registered last, so they tear down first).
|
||||||
|
// Each '-command_id' re-presents the SAME interned, channel-qualified id (channelIdFor
|
||||||
|
// returns the memoized pointer registered above), so the unregister matches exactly.
|
||||||
|
rec->Register("-gaccel", (void*)&g_accelUntagItems);
|
||||||
|
rec->Register("-command_id", (void*)channelIdFor(kIdUntagItems));
|
||||||
|
rec->Register("-gaccel", (void*)&g_accelMoveItemsArrange);
|
||||||
|
rec->Register("-command_id", (void*)channelIdFor(kIdMoveItemsArrange));
|
||||||
|
rec->Register("-gaccel", (void*)&g_accelMoveItemsDesign);
|
||||||
|
rec->Register("-command_id", (void*)channelIdFor(kIdMoveItemsDesign));
|
||||||
|
rec->Register("-gaccel", (void*)&g_accelShowBoth);
|
||||||
|
rec->Register("-command_id", (void*)channelIdFor(kIdShowBoth));
|
||||||
|
rec->Register("-gaccel", (void*)&g_accelUntag);
|
||||||
|
rec->Register("-command_id", (void*)channelIdFor(kIdUntag));
|
||||||
|
rec->Register("-gaccel", (void*)&g_accelTagArrange);
|
||||||
|
rec->Register("-command_id", (void*)channelIdFor(kIdTagArrange));
|
||||||
|
rec->Register("-gaccel", (void*)&g_accelTagDesign);
|
||||||
|
rec->Register("-command_id", (void*)channelIdFor(kIdTagDesign));
|
||||||
|
rec->Register("-gaccel", (void*)&g_accelActivateDesign);
|
||||||
|
rec->Register("-command_id", (void*)channelIdFor(kIdActivateDesign));
|
||||||
|
rec->Register("-gaccel", (void*)&g_accelActivateArrange);
|
||||||
|
rec->Register("-command_id", (void*)channelIdFor(kIdActivateArrange));
|
||||||
|
rec->Register("-gaccel", (void*)&g_accelToggleMode);
|
||||||
|
rec->Register("-command_id", (void*)channelIdFor(kIdToggleMode));
|
||||||
|
|
||||||
|
g_session = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
// design_view_actions — the Design View action family (Phase D4; Q-W4 split of
|
||||||
|
// actions.h). Registers the bindable actions that drive the mode workflow and wires
|
||||||
|
// them end-to-end: toggle/activate a mode, tag/untag/show-both the current track
|
||||||
|
// selection, and the item-level mode moves (D2 W3-B). Each action mutates the
|
||||||
|
// session's ViewModeModel (D1, via persist's ReaSamplerSession) and then reapplies
|
||||||
|
// the active mode through the view shell (D2) so the change takes effect immediately.
|
||||||
|
//
|
||||||
|
// REAPER-facing shell: the .cpp includes reaper_plugin_functions.h WITHOUT
|
||||||
|
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers — CLAUDE.md §contract). This
|
||||||
|
// header is SDK-free; main.cpp calls register/handle/unregister and nothing else.
|
||||||
|
|
||||||
|
// Forward-declared at GLOBAL scope (matches reaper_plugin.h's typedef struct
|
||||||
|
// reaper_plugin_info_t) so this header stays SDK-free; the .cpp includes the real
|
||||||
|
// definition. Declared before the namespace so it is the global type, not a
|
||||||
|
// namespace-local shadow.
|
||||||
|
struct reaper_plugin_info_t;
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
class ReaSamplerSession;
|
||||||
|
|
||||||
|
// Registers the Design View action family against `rec` (command_id + gaccel +
|
||||||
|
// hookcommand-routing is owned by the caller's single hookcommand). `session` is the
|
||||||
|
// live session the actions mutate; it must outlive the registration. Idempotent is
|
||||||
|
// NOT promised — call exactly once at load, mirror-unregister once at unload.
|
||||||
|
void designViewRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session);
|
||||||
|
|
||||||
|
// Services one fired command. Returns true iff `command` is one of this module's
|
||||||
|
// action ids (and it was handled); false otherwise so the caller's hookcommand keeps
|
||||||
|
// looking (per the contract: claim only our own ids). Safe to call for any command.
|
||||||
|
bool designViewHandleCommand(int command);
|
||||||
|
|
||||||
|
// Mirror-unregisters everything designViewRegisterActions registered, with the
|
||||||
|
// '-'-prefixed strings (per the contract's unload rule). Call once on rec==nullptr.
|
||||||
|
void designViewUnregisterActions(reaper_plugin_info_t* rec);
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// prune_action.cpp — the "Prune bank folder" action body (Phase R3; Q-W4 split of
|
||||||
|
// actions.cpp). See prune_action.h for the contract this TU preserves.
|
||||||
|
//
|
||||||
|
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||||
|
// WITHOUT REAPERAPI_IMPLEMENT (main.cpp owns the API pointers — CLAUDE.md §contract).
|
||||||
|
|
||||||
|
#include "shell/actions/prune_action.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "persist.h" // ReaSamplerSession — pruneDryRun / pruneOrphanSet / pruneReclaim
|
||||||
|
|
||||||
|
#define REAPERAPI_MINIMAL
|
||||||
|
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||||
|
#define REAPERAPI_WANT_ShowMessageBox
|
||||||
|
#include "reaper_plugin_functions.h"
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
// Prune bank folder — Phase R (Reclaim), R3: the guarded DESTRUCTIVE step, and the SOLE
|
||||||
|
// file-deletion entry in ReaSampler. Dry-run FIRST (compute the orphan set, read-only),
|
||||||
|
// then — only when orphans exist — a blocking CONFIRM showing the SPECIFIC manifest
|
||||||
|
// (count + reclaimable bytes + the file list, truncated consistent with the 64-cap), then
|
||||||
|
// on explicit Yes delete EXACTLY that set (session.pruneReclaim, which recomputes the
|
||||||
|
// pure core fresh and deletes confirmed ∩ freshOrphans — trash-preferred, unlink fallback).
|
||||||
|
// Zero orphans => informational only, NO confirm ever shown. Cancel deletes nothing.
|
||||||
|
//
|
||||||
|
// The full (untruncated) orphan set is captured here for the delete; the dry-run's
|
||||||
|
// truncated list is only the confirm's readout. No ext-state is written and no undo point
|
||||||
|
// is opened (file deletion is not REAPER-undoable and pruneReclaim mutates no project
|
||||||
|
// state) — a Ctrl-Z after a prune correctly cannot claim to restore deleted files.
|
||||||
|
void doBankPruneFolder(ReaSamplerSession& session) {
|
||||||
|
const PruneReport report = session.pruneDryRun();
|
||||||
|
|
||||||
|
// pS-usage FAIL-SAFE: a present instance-usage record could not be read — the
|
||||||
|
// protected set is unknowable, so the prune HALTS outright (deletes nothing) rather
|
||||||
|
// than proceed with degraded protection. Distinct from "no orphans": the user must
|
||||||
|
// know the prune refused to run and why.
|
||||||
|
if (report.abortedUnreadableUsage) {
|
||||||
|
std::string msg =
|
||||||
|
"ReaSampler prune: ABORTED -- one or more instance usage records could not "
|
||||||
|
"be read or decoded. Nothing was deleted.\n"
|
||||||
|
"If the owning instance is still loaded it will republish its record on the "
|
||||||
|
"next poll tick, clearing the abort. If the instance no longer exists (the "
|
||||||
|
"key is an orphaned corrupt record), clear it manually via ReaScript:\n"
|
||||||
|
" reaper.SetProjExtState(0, \"reasampler\", \"<key>\", \"\")\n"
|
||||||
|
"Offending key(s):\n";
|
||||||
|
for (const std::string& key : report.offendingUsageKeys) {
|
||||||
|
msg += " " + key + "\n";
|
||||||
|
}
|
||||||
|
ShowConsoleMsg(msg.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report.count == 0) {
|
||||||
|
ShowConsoleMsg("ReaSampler prune: no orphaned files to reclaim.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The EXACT set the delete will target — full, untruncated, so what the confirm
|
||||||
|
// summarises (count + bytes) matches what pruneReclaim reclaims. Captured before the
|
||||||
|
// confirm so the confirm and the delete reason about the same enumeration.
|
||||||
|
const std::vector<std::string> orphanSet = session.pruneOrphanSet();
|
||||||
|
|
||||||
|
// Confirm-with-manifest: count + bytes exact; the file list is the dry-run's 64-capped
|
||||||
|
// list (the same clip the R2 readout used), with a "N more not shown" tail when clipped.
|
||||||
|
std::string msg =
|
||||||
|
"ReaSampler prune will PERMANENTLY reclaim " + std::to_string(report.count) +
|
||||||
|
" orphaned file(s), freeing " + std::to_string(report.totalBytes) + " bytes.\n\n"
|
||||||
|
"These files are no longer referenced by any bank and were created by ReaSampler.\n"
|
||||||
|
"They will be moved to the Recycle Bin on Windows (recoverable), or deleted on "
|
||||||
|
"other platforms.\n\n";
|
||||||
|
for (const std::string& rel : report.orphans) msg += " " + rel + "\n";
|
||||||
|
if (report.truncated) {
|
||||||
|
msg += " ... (" + std::to_string(report.count - report.orphans.size()) +
|
||||||
|
" more not shown)\n";
|
||||||
|
}
|
||||||
|
msg += "\nReclaim these files now?";
|
||||||
|
|
||||||
|
const int r = ShowMessageBox(msg.c_str(), "ReaSampler: prune bank folder", 4);
|
||||||
|
if (r != 6) { // 6 == YES; anything else cancels -> delete NOTHING (SDK ~6544)
|
||||||
|
ShowConsoleMsg("ReaSampler prune: cancelled -- nothing deleted.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmed -> delete exactly the confirmed set (recomputed fresh, stale entries skipped).
|
||||||
|
const PruneDeletionResult del = session.pruneReclaim(orphanSet);
|
||||||
|
|
||||||
|
std::string done = "ReaSampler prune: reclaimed " +
|
||||||
|
std::to_string(del.reclaimedCount) + " file(s), " +
|
||||||
|
std::to_string(del.reclaimedBytes) + " bytes" +
|
||||||
|
(del.usedTrash ? " (to Recycle Bin)" : " (deleted)") + ".";
|
||||||
|
if (del.skippedCount > 0) {
|
||||||
|
done += " " + std::to_string(del.skippedCount) +
|
||||||
|
" file(s) skipped (locked, or changed since the report).";
|
||||||
|
}
|
||||||
|
done += "\n";
|
||||||
|
ShowConsoleMsg(done.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
// prune_action — the "Prune bank folder" action body (Phase R3; Q-W4 split of
|
||||||
|
// actions.cpp). This is the SOLE file-deletion action in ReaSampler, isolated in its
|
||||||
|
// own TU so the deletion authority is one obvious module on the actions side (its
|
||||||
|
// persist-side counterpart concentrates into prune_fs in Q-W5). Registration and
|
||||||
|
// hookcommand routing for its FOREVER-STABLE id (BANK_PRUNE_FOLDER) stay with the
|
||||||
|
// bank family (bank_actions) — one registration flow, one guarded body here.
|
||||||
|
//
|
||||||
|
// Contract (preserve exactly): dry-run first; abort outright on unreadable usage
|
||||||
|
// records (pS-usage fail-safe); confirm-with-manifest before any deletion; opens NO
|
||||||
|
// undo point and writes NO ext state (file deletion is not REAPER-undoable). Routes
|
||||||
|
// to persist's public session API only (pruneDryRun / pruneOrphanSet / pruneReclaim).
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
class ReaSamplerSession;
|
||||||
|
|
||||||
|
// Runs the guarded prune flow against `session`. Called by the bank family's
|
||||||
|
// hookcommand handler when the BANK_PRUNE_FOLDER action fires.
|
||||||
|
void doBankPruneFolder(ReaSamplerSession& session);
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -331,7 +331,7 @@ std::string RunCapture(ReaSamplerSession& session, const CaptureActionDef& def)
|
|||||||
// An undo that removes the captured sample also clears the assign_request that named it,
|
// An undo that removes the captured sample also clears the assign_request that named it,
|
||||||
// preventing a stale request from pointing at a removed sample. The block uses the house
|
// preventing a stale request from pointing at a removed sample. The block uses the house
|
||||||
// pattern (UNDO_STATE_MISCCFG, discarded on an unsaved project with empty label + zero
|
// pattern (UNDO_STATE_MISCCFG, discarded on an unsaved project with empty label + zero
|
||||||
// flag) matching the bank-op family in actions.cpp.
|
// flag) matching the bank-op family (persistBankOp, panel_bank_ops).
|
||||||
void RunCaptureItemAssign(ReaSamplerSession& session)
|
void RunCaptureItemAssign(ReaSamplerSession& session)
|
||||||
{
|
{
|
||||||
// Reuse the Item-scope def from the capture table (index 0) — same range logic, same
|
// Reuse the Item-scope def from the capture table (index 0) — same range logic, same
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "core/namespaces.h"
|
#include "core/namespaces.h"
|
||||||
// track_guid — the ONE place a MediaTrack* is formatted into the canonical GUID
|
// track_guid — the ONE place a MediaTrack* is formatted into the canonical GUID
|
||||||
// string used as a membership-index key. Both the Design View shell (view.cpp) and
|
// string used as a membership-index key. Both the Design View shell (view.cpp) and
|
||||||
// the actions layer (actions.cpp) key membership on this exact string, so the key
|
// the actions layer (design_view_actions.cpp) key membership on this exact string, so the key
|
||||||
// contract lives in a single helper rather than being re-derived (and drifting) at
|
// contract lives in a single helper rather than being re-derived (and drifting) at
|
||||||
// two call sites (the cross-module key contract flagged in D2 review).
|
// two call sites (the cross-module key contract flagged in D2 review).
|
||||||
//
|
//
|
||||||
|
|||||||
+218
-104
@@ -1,13 +1,15 @@
|
|||||||
// panel_bank_ops.cpp — the bank-CRUD + menus seam of the docked bank panel (Q-W2
|
// panel_bank_ops.cpp — the bank-CRUD + menus seam of the docked bank panel (Q-W2
|
||||||
// split of bank_panel.cpp; Phase B4/B5). The SINGLE home of the panel-side bank verbs
|
// split of bank_panel.cpp; Phase B4/B5). Since Q-W4 this TU is the ONE implementation
|
||||||
// (create / rename / delete / evacuate / activate / move / copy / remove) — the owner
|
// home of the bank verbs (create / rename / delete / evacuate / activate / move /
|
||||||
// Q-W4 dedupes actions.cpp against — plus the book/bank accessors, the popup menus
|
// copy / remove): the promptless bankOp* inner verbs (model op + persistBankOp only)
|
||||||
// that drive them, and the selection-id / OS-drag path resolvers.
|
// serve BOTH thin UX skins — the panel's menu handlers here and the bindable
|
||||||
|
// bank_actions family — plus the book/bank accessors, the popup menus that drive
|
||||||
|
// them, and the selection-id / OS-drag path resolvers.
|
||||||
//
|
//
|
||||||
// Each op mutates g_session.book() then persists via persistBankOp() (one bank op =
|
// Each verb mutates the session's book() then persists via persistBankOp() (one bank
|
||||||
// one Ctrl-Z; a true index no-op opens NO undo point). It DOES mutate the bank BOOK —
|
// op = one Ctrl-Z; a true index no-op opens NO undo point). It DOES mutate the bank
|
||||||
// that is the whole point of B4 — but only the index/model + ext-state, never the
|
// BOOK — that is the whole point of B4 — but only the index/model + ext-state, never
|
||||||
// arrange, never a sample file on disk (bank ops are index-only; files stay put —
|
// the arrange, never a sample file on disk (bank ops are index-only; files stay put —
|
||||||
// CONTEXT.md §Multi-bank). REFERENCE-INVALIDATION GUARDRAIL: after a STRUCTURAL
|
// CONTEXT.md §Multi-bank). REFERENCE-INVALIDATION GUARDRAIL: after a STRUCTURAL
|
||||||
// mutation any Bank*/BankModel& is invalid — resolve fresh, pass ids.
|
// mutation any Bank*/BankModel& is invalid — resolve fresh, pass ids.
|
||||||
//
|
//
|
||||||
@@ -23,7 +25,6 @@
|
|||||||
#include "shell/panel/panel_state.h"
|
#include "shell/panel/panel_state.h"
|
||||||
#include "shell/panel/panel_bank_ops.h"
|
#include "shell/panel/panel_bank_ops.h"
|
||||||
|
|
||||||
#include "actions.h" // persistBankOp — shared undo-block wrapper (R-B panel path)
|
|
||||||
#include "persist.h" // ReaSamplerSession — the live session the ops mutate
|
#include "persist.h" // ReaSamplerSession — the live session the ops mutate
|
||||||
|
|
||||||
#define REAPERAPI_MINIMAL
|
#define REAPERAPI_MINIMAL
|
||||||
@@ -33,6 +34,8 @@
|
|||||||
#define REAPERAPI_WANT_Main_OnCommand
|
#define REAPERAPI_WANT_Main_OnCommand
|
||||||
#define REAPERAPI_WANT_genGuid
|
#define REAPERAPI_WANT_genGuid
|
||||||
#define REAPERAPI_WANT_guidToString
|
#define REAPERAPI_WANT_guidToString
|
||||||
|
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||||
|
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||||
#include "reaper_plugin_functions.h"
|
#include "reaper_plugin_functions.h"
|
||||||
|
|
||||||
namespace reasampler::panel {
|
namespace reasampler::panel {
|
||||||
@@ -79,55 +82,28 @@ std::vector<const Bank*> namedBanks() {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Bank management ops (id-keyed; drive the B1 model + persist) --------------
|
// --- Bank management ops (id-keyed; THIN UX SKINS over the bankOp* verbs) ------
|
||||||
//
|
//
|
||||||
// Each op mutates g_session.book() then persists via persistBankOp(). After a
|
// Q-W4: each handler here owns only the panel's UX (prompts / confirms / message
|
||||||
// STRUCTURAL mutation (create/delete/evacuate) any Bank*/BankModel& is invalid — we
|
// boxes / panel-state nudges / repaint); the model op + persist is the shared
|
||||||
// resolve fresh, pass ids, and let the next refreshFingerprint repaint. On an
|
// bankOp* inner verb (defined in the public section below). After a STRUCTURAL
|
||||||
// unsaved project the empty-close discard in persistBankOp ensures no stale state
|
// mutation (create/delete/evacuate) any Bank*/BankModel& is invalid — we resolve
|
||||||
|
// fresh, pass ids, and let the next refreshFingerprint repaint. On an unsaved
|
||||||
|
// project the empty-close discard in persistBankOp ensures no stale state
|
||||||
// survives (matches the capture/B3 quiet-persist idiom).
|
// survives (matches the capture/B3 quiet-persist idiom).
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// REAPER's stock single-line input (comma-safe via the \x1f return separator, as B3).
|
|
||||||
bool promptText(const char* title, const char* caption, const std::string& initial,
|
|
||||||
std::string& out) {
|
|
||||||
std::vector<char> buf(512, '\0');
|
|
||||||
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;
|
|
||||||
std::string s(buf.data());
|
|
||||||
if (s.empty()) return false;
|
|
||||||
out = std::move(s);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mints a genuine REAPER GUID string as a stable bank id (same as B3 mintBankId).
|
|
||||||
std::string mintBankId() {
|
|
||||||
GUID g{};
|
|
||||||
genGuid(&g);
|
|
||||||
char buf[64] = {0};
|
|
||||||
guidToString(&g, buf);
|
|
||||||
return std::string(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void doCreateBank() {
|
void doCreateBank() {
|
||||||
if (!book()) return;
|
if (!book()) return;
|
||||||
std::string name;
|
std::string name;
|
||||||
if (!promptText("ReaSampler: create bank", "Bank name:", "", name)) return;
|
if (!promptText("ReaSampler: create bank", "Bank name:", "", name)) return;
|
||||||
const std::string id = mintBankId();
|
const std::string id = bankOpCreate(name);
|
||||||
if (!book()->createBank(id, name)) {
|
if (id.empty()) {
|
||||||
ShowMessageBox("A bank with that name already exists.",
|
ShowMessageBox("A bank with that name already exists.",
|
||||||
"ReaSampler: create bank", 0);
|
"ReaSampler: create bank", 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
g_panel.shownBankId = id; // show the freshly-created bank
|
g_panel.shownBankId = id; // show the freshly-created bank
|
||||||
g_panel.focusedRegion = Region::Banks;
|
g_panel.focusedRegion = Region::Banks;
|
||||||
persistBankOp("ReaSampler: create bank");
|
|
||||||
invalidatePanel();
|
invalidatePanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,12 +116,11 @@ void doRenameBank(const std::string& bankId) {
|
|||||||
const std::string current = bk->displayName; // copy before any mutation
|
const std::string current = bk->displayName; // copy before any mutation
|
||||||
std::string newName;
|
std::string newName;
|
||||||
if (!promptText("ReaSampler: rename bank", "New name:", current, newName)) return;
|
if (!promptText("ReaSampler: rename bank", "New name:", current, newName)) return;
|
||||||
if (!book()->renameBank(bankId, newName)) {
|
if (!bankOpRename(bankId, newName)) {
|
||||||
ShowMessageBox("Another bank already uses that name.",
|
ShowMessageBox("Another bank already uses that name.",
|
||||||
"ReaSampler: rename bank", 0);
|
"ReaSampler: rename bank", 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
persistBankOp("ReaSampler: rename bank");
|
|
||||||
invalidatePanel();
|
invalidatePanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,11 +152,11 @@ void doDeleteBank(const std::string& bankId) {
|
|||||||
}
|
}
|
||||||
// r == 6 (Yes) falls through to a plain delete (drops members).
|
// r == 6 (Yes) falls through to a plain delete (drops members).
|
||||||
}
|
}
|
||||||
if (!book()->deleteBank(bankId)) return;
|
|
||||||
// S9: bump when the bank held samples (either the Yes-drop path or the No-evacuate-then-
|
// S9: bump when the bank held samples (either the Yes-drop path or the No-evacuate-then-
|
||||||
// delete path moved/dropped members) — both change what a live instance could play. An
|
// delete path moved/dropped members) — both change what a live instance could play. An
|
||||||
// empty-bank delete is purely organizational, no bump.
|
// empty-bank delete is purely organizational, no bump. The ORIGINAL member count decides
|
||||||
persistBankOp("ReaSampler: delete bank", /*bumpGeneration=*/members > 0);
|
// (the No-path evacuated them moments ago, but the membership still changed).
|
||||||
|
if (!bankOpDelete(bankId, /*bumpGeneration=*/members > 0)) return;
|
||||||
// shownBankId is reconciled by the next fingerprint pass. If no named banks remain,
|
// shownBankId is reconciled by the next fingerprint pass. If no named banks remain,
|
||||||
// nudge focus to the pool so the selection has a valid home.
|
// nudge focus to the pool so the selection has a valid home.
|
||||||
if (namedBanks().empty()) g_panel.focusedRegion = Region::Pool;
|
if (namedBanks().empty()) g_panel.focusedRegion = Region::Pool;
|
||||||
@@ -192,79 +167,39 @@ void doEvacuateBank(const std::string& bankId) {
|
|||||||
if (!book()) return;
|
if (!book()) return;
|
||||||
const Bank* bk = book()->bank(bankId);
|
const Bank* bk = book()->bank(bankId);
|
||||||
if (!bk || bk->isPool()) return;
|
if (!bk || bk->isPool()) return;
|
||||||
if (!book()->evacuate(bankId)) return;
|
if (!bankOpEvacuate(bankId)) return;
|
||||||
persistBankOp("ReaSampler: evacuate bank", /*bumpGeneration=*/true); // S9: membership changed
|
|
||||||
invalidatePanel();
|
invalidatePanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
void doActivateBank(const std::string& bankId) {
|
void doActivateBank(const std::string& bankId) {
|
||||||
if (!book()) return;
|
if (!bankOpActivate(bankId)) return; // rejects an unknown id
|
||||||
if (!book()->setActiveBank(bankId)) return; // rejects an unknown id
|
|
||||||
persistBankOp("ReaSampler: activate bank");
|
|
||||||
invalidatePanel();
|
invalidatePanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// Move or copy `sampleIds` from `srcBankId` to `destBankId` (index-only). Both pass
|
// Move or copy `sampleIds` from `srcBankId` to `destBankId` (index-only). Thin panel
|
||||||
// ids straight to the model op (no BankModel& cached across the loop's mutations).
|
// skin over bankOpTransfer (the one-home verb owns the loop, the verb-aware no-op
|
||||||
//
|
// guardrail, and the undo-batched persist); this layer clears the stale selection
|
||||||
// NO-OP GUARDRAIL — VERB-AWARE (matches the action layer's doBankTransferSelected):
|
// and repaints on an actual mutation.
|
||||||
// * MOVE collapse: the source entry WAS removed (bank_book removes unconditionally
|
|
||||||
// before the dest add collapses on hash), so the index DID mutate — counts.
|
|
||||||
// * COPY collapse: the source is left intact AND the dest already held the hash,
|
|
||||||
// so NOTHING changed — a true index no-op. Must NOT open an undo point.
|
|
||||||
// Hence: copy counts only real gains (Copied); move counts gains OR collapses.
|
|
||||||
void transferSamples(const std::vector<std::string>& sampleIds,
|
void transferSamples(const std::vector<std::string>& sampleIds,
|
||||||
const std::string& srcBankId, const std::string& destBankId,
|
const std::string& srcBankId, const std::string& destBankId,
|
||||||
bool copy) {
|
bool copy) {
|
||||||
if (!book()) return;
|
if (!bankOpTransfer(sampleIds, srcBankId, destBankId, copy))
|
||||||
if (sampleIds.empty() || srcBankId == destBankId) return;
|
return; // nothing changed — no persist, no undo point
|
||||||
if (!book()->bank(srcBankId) || !book()->bank(destBankId)) return;
|
|
||||||
int ok = 0, collapsed = 0;
|
|
||||||
for (const std::string& sid : sampleIds) {
|
|
||||||
const TransferResult r =
|
|
||||||
copy ? book()->copySample(sid, srcBankId, destBankId)
|
|
||||||
: book()->moveSample(sid, srcBankId, destBankId);
|
|
||||||
switch (r) {
|
|
||||||
case TransferResult::Moved:
|
|
||||||
case TransferResult::Copied: ++ok; break;
|
|
||||||
case TransferResult::Collapsed: ++collapsed; break;
|
|
||||||
case TransferResult::RejectedUnknownBank:
|
|
||||||
case TransferResult::RejectedSampleAbsent:
|
|
||||||
case TransferResult::RejectedSameBank: break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const bool mutated = copy ? (ok > 0) : (ok > 0 || collapsed > 0);
|
|
||||||
if (!mutated) return; // nothing changed — no persist, no undo point
|
|
||||||
|
|
||||||
const char* label = copy ? "ReaSampler: copy sample(s)" : "ReaSampler: move sample(s)";
|
|
||||||
persistBankOp(label, /*bumpGeneration=*/true); // S9: bank membership changed
|
|
||||||
// The selection indexed into the source; after a move those indices are stale, so
|
// The selection indexed into the source; after a move those indices are stale, so
|
||||||
// clear it (the fingerprint pass will also clear, but do it now for immediacy).
|
// clear it (the fingerprint pass will also clear, but do it now for immediacy).
|
||||||
g_panel.selection = Selection{};
|
g_panel.selection = Selection{};
|
||||||
invalidatePanel();
|
invalidatePanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove `sampleIds` from `srcBankId` (index-only, this-bank scope). Non-destructive to
|
// Remove `sampleIds` from `srcBankId` (index-only, this-bank scope). Thin panel skin
|
||||||
// the file: a last-reference remove leaves the file on disk, orphaned until Phase R
|
// over bankOpRemove — see the verb for the never-deletes-bytes / silent-remove /
|
||||||
// prune — remove NEVER deletes bytes (the manifest is untouched). Removes are silent
|
// one-Ctrl-Z contract. Clears the stale selection and repaints on an actual removal.
|
||||||
// (no confirm dialog); recoverability is provided by the batched REAPER undo (R-B) —
|
|
||||||
// one Ctrl-Z restores the index entry. Ids passed by value — no BankModel& cached
|
|
||||||
// across the loop's mutations.
|
|
||||||
void removeSamples(const std::vector<std::string>& sampleIds,
|
void removeSamples(const std::vector<std::string>& sampleIds,
|
||||||
const std::string& srcBankId) {
|
const std::string& srcBankId) {
|
||||||
if (!book() || sampleIds.empty()) return;
|
if (!bankOpRemove(sampleIds, srcBankId))
|
||||||
if (!book()->bank(srcBankId)) return;
|
return; // nothing changed — no persist, no undo point
|
||||||
|
|
||||||
int removed = 0;
|
|
||||||
for (const std::string& sid : sampleIds)
|
|
||||||
if (book()->removeSample(sid, srcBankId, RemoveScope::ThisBank) ==
|
|
||||||
RemoveResult::Removed)
|
|
||||||
++removed;
|
|
||||||
if (removed == 0) return; // nothing changed — no persist, no undo point
|
|
||||||
|
|
||||||
persistBankOp("ReaSampler: remove sample(s)", /*bumpGeneration=*/true); // S9: sample dropped
|
|
||||||
// The selection indexed into the source; after a remove those indices are stale, so
|
// The selection indexed into the source; after a remove those indices are stale, so
|
||||||
// clear it (the fingerprint pass will also clear, but do it now for immediacy).
|
// clear it (the fingerprint pass will also clear, but do it now for immediacy).
|
||||||
g_panel.selection = Selection{};
|
g_panel.selection = Selection{};
|
||||||
@@ -471,10 +406,189 @@ void showSelectionMenu(int screenX, int screenY) {
|
|||||||
|
|
||||||
} // namespace reasampler::panel
|
} // namespace reasampler::panel
|
||||||
|
|
||||||
// --- Public API (the selection read seam — panel_bank_ops.h) -------------------
|
// --- Public API (panel_bank_ops.h) ---------------------------------------------
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Persists the book after a bank mutation. Mirrors the CAPTURE path, NOT the
|
||||||
|
// Design-View path: quiet persist — saveToActiveProject no-ops on an unsaved project
|
||||||
|
// (the change stays valid for the session and persists on the user's next save).
|
||||||
|
// Deliberately NO Save-As prompt; do not "align" with persistViewState's prompt
|
||||||
|
// idiom. Returns whether a persist actually happened, so persistBankOp can discard
|
||||||
|
// its undo block when nothing was written. Session pointer is live for the whole
|
||||||
|
// extension lifetime (bankPanelInit at load, before any action registers).
|
||||||
|
bool persistBook() { return panel::g_panel.session->saveToActiveProject(); }
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// One home (Q-W4) for the former actions.cpp/panel_bank_ops.cpp byte-identical twins.
|
||||||
|
// COMMA GUARD: GetUserInputs splits returned values on a separator defaulting to ',',
|
||||||
|
// so the return separator is overridden to \x1f (un-typeable) via the documented
|
||||||
|
// `separator=X` trailing pseudo-caption (SDK ~3806) — any printable name round-trips.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persists a completed bank-index verb as a SINGLE batched REAPER undo point (R-B) —
|
||||||
|
// one bank op = one Ctrl-Z.
|
||||||
|
//
|
||||||
|
// WHY THIS WRAPS AND persistBook() DOES NOT: a bank verb mutates ONLY our project
|
||||||
|
// ext-state (SetProjExtState under "reasampler"), which REAPER's undo system captures
|
||||||
|
// iff UNDO_STATE_MISCCFG is set in the Undo_EndBlock2 flags — the SDK documents
|
||||||
|
// MISCCFG as covering "extensions!" project ext-state (reaper_plugin.h ~1544, ~1199).
|
||||||
|
// We pass exactly UNDO_STATE_MISCCFG (not -1 / UNDO_STATE_ALL as the item-move family
|
||||||
|
// does): a bank verb touches no tracks, FX, items, or envelopes, so snapshotting them
|
||||||
|
// would be both heavier and semantically wrong. persistBook() (= SetProjExtState) runs
|
||||||
|
// INSIDE the block so the post-mutation ext-state is the block's "after" image.
|
||||||
|
//
|
||||||
|
// UNSAVED-PROJECT GUARDRAIL: on an unsaved / no-active project persistBook() no-ops
|
||||||
|
// (nothing is written to ext state). We must still CLOSE the block we opened, but with
|
||||||
|
// an EMPTY label and a zero flag so REAPER DISCARDS the point instead of recording a
|
||||||
|
// no-effect undo entry — mirroring view.cpp's empty-plan close. The in-session model
|
||||||
|
// change stands and persists on the user's next save; it just earns no undo point until
|
||||||
|
// there is a project to persist into (undo of an unsaved bank op has nothing to roll
|
||||||
|
// back to anyway). The Begin/End must still be balanced, hence the close-either-way.
|
||||||
|
void persistBankOp(const char* label, bool bumpGeneration) {
|
||||||
|
Undo_BeginBlock2(nullptr);
|
||||||
|
// S9: bump the bank-generation counter INSIDE the block, before persistBook(), so the
|
||||||
|
// fresh generation rides the same ext-state write the persist makes (persistBook() ->
|
||||||
|
// saveToActiveProject() stamps bankGeneration()). Bumped only for content-changing verbs
|
||||||
|
// (the caller decides); a pure-organizational verb passes false and leaves the counter be,
|
||||||
|
// so a rename/activate does not needlessly refresh live instances.
|
||||||
|
if (bumpGeneration) panel::g_panel.session->bumpBankGeneration();
|
||||||
|
const bool persisted = persistBook();
|
||||||
|
if (persisted)
|
||||||
|
Undo_EndBlock2(nullptr, label, UNDO_STATE_MISCCFG);
|
||||||
|
else
|
||||||
|
Undo_EndBlock2(nullptr, "", 0); // no ext-state write -> discard the empty point
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Promptless inner bank verbs (Q-W4 single home) ----------------------------
|
||||||
|
// Model op + persistBankOp only; NO UX. Callers own prompts/confirms/nudges. Each
|
||||||
|
// verb resolves the book fresh (panel::book(), null when no live session) and
|
||||||
|
// persists ONLY after the model accepted — a rejected op opens no undo point.
|
||||||
|
|
||||||
|
std::string bankOpCreate(const std::string& name) {
|
||||||
|
BankBook* b = panel::book();
|
||||||
|
if (!b) return {};
|
||||||
|
const std::string id = mintBankId();
|
||||||
|
if (!b->createBank(id, name)) return {}; // duplicate display name (model rule)
|
||||||
|
persistBankOp("ReaSampler: create bank");
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bankOpRename(const std::string& bankId, const std::string& newName) {
|
||||||
|
BankBook* b = panel::book();
|
||||||
|
if (!b || !b->renameBank(bankId, newName)) return false; // pool / name in use
|
||||||
|
persistBankOp("ReaSampler: rename bank");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bankOpDelete(const std::string& bankId, bool bumpGeneration) {
|
||||||
|
BankBook* b = panel::book();
|
||||||
|
if (!b || !b->deleteBank(bankId)) return false; // pool un-deletable (model rule)
|
||||||
|
persistBankOp("ReaSampler: delete bank", bumpGeneration);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bankOpEvacuate(const std::string& bankId) {
|
||||||
|
BankBook* b = panel::book();
|
||||||
|
if (!b || !b->evacuate(bankId)) return false; // pool is a destination, not a source
|
||||||
|
// S9: evacuate moves members between banks (bank membership changes) -> bump.
|
||||||
|
persistBankOp("ReaSampler: evacuate bank", /*bumpGeneration=*/true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bankOpActivate(const std::string& bankId) {
|
||||||
|
BankBook* b = panel::book();
|
||||||
|
if (!b || !b->setActiveBank(bankId)) return false; // rejects an unknown id
|
||||||
|
persistBankOp("ReaSampler: activate bank");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NO-OP GUARDRAIL — VERB-AWARE (a collapse means different things per verb):
|
||||||
|
// * MOVE collapse: the source entry WAS removed (bank_book moveSample removes
|
||||||
|
// unconditionally before the dest add collapses on hash), so the index DID
|
||||||
|
// mutate — it counts toward opening an undo point.
|
||||||
|
// * COPY collapse: the source is left intact AND the dest already held the hash,
|
||||||
|
// so NOTHING changed — a true index no-op. It must NOT open an undo point.
|
||||||
|
// Hence: copy counts only real gains; move counts gains OR collapses. Ids pass
|
||||||
|
// straight to the model op — no BankModel& cached across the loop's mutations.
|
||||||
|
bool bankOpTransfer(const std::vector<std::string>& sampleIds,
|
||||||
|
const std::string& srcBankId, const std::string& destBankId,
|
||||||
|
bool copy) {
|
||||||
|
BankBook* b = panel::book();
|
||||||
|
if (!b || sampleIds.empty() || srcBankId == destBankId) return false;
|
||||||
|
if (!b->bank(srcBankId) || !b->bank(destBankId)) return false;
|
||||||
|
int ok = 0, collapsed = 0;
|
||||||
|
for (const std::string& sid : sampleIds) {
|
||||||
|
const TransferResult r =
|
||||||
|
copy ? b->copySample(sid, srcBankId, destBankId)
|
||||||
|
: b->moveSample(sid, srcBankId, destBankId);
|
||||||
|
switch (r) {
|
||||||
|
case TransferResult::Moved:
|
||||||
|
case TransferResult::Copied: ++ok; break;
|
||||||
|
case TransferResult::Collapsed: ++collapsed; break;
|
||||||
|
case TransferResult::RejectedUnknownBank:
|
||||||
|
case TransferResult::RejectedSampleAbsent:
|
||||||
|
case TransferResult::RejectedSameBank: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bool mutated = copy ? (ok > 0) : (ok > 0 || collapsed > 0);
|
||||||
|
if (!mutated) return false; // nothing changed — no persist, no undo point
|
||||||
|
// S9: a move/copy changes bank membership (a sample arrives in / leaves a bank an
|
||||||
|
// instance may reference) -> bump so assigned instances refresh hands-free.
|
||||||
|
persistBankOp(copy ? "ReaSampler: copy sample(s)" : "ReaSampler: move sample(s)",
|
||||||
|
/*bumpGeneration=*/true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index-only, this-bank scope (fork R-A: the sole surfaced verb; RemoveScope::AllBanks
|
||||||
|
// stays latent in the model). Non-destructive to the file: a last-reference remove
|
||||||
|
// leaves the file on disk, orphaned until Phase R prune — remove NEVER deletes bytes
|
||||||
|
// (the manifest is untouched). Silent: recoverability is the batched undo (R-B).
|
||||||
|
bool bankOpRemove(const std::vector<std::string>& sampleIds,
|
||||||
|
const std::string& srcBankId) {
|
||||||
|
BankBook* b = panel::book();
|
||||||
|
if (!b || sampleIds.empty() || !b->bank(srcBankId)) return false;
|
||||||
|
int removed = 0;
|
||||||
|
for (const std::string& sid : sampleIds)
|
||||||
|
if (b->removeSample(sid, srcBankId, RemoveScope::ThisBank) ==
|
||||||
|
RemoveResult::Removed)
|
||||||
|
++removed;
|
||||||
|
if (removed == 0) return false; // every id already absent — no undo point
|
||||||
|
// S9: a remove drops a sample from a bank (an instance referencing it must refresh —
|
||||||
|
// it will resolve to silence, per the stale-id policy) -> bump.
|
||||||
|
persistBankOp("ReaSampler: remove sample(s)", /*bumpGeneration=*/true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Selection read seam --------------------------------------------------------
|
||||||
|
|
||||||
std::vector<std::string> bankPanelSelectedSampleIds() {
|
std::vector<std::string> bankPanelSelectedSampleIds() {
|
||||||
return panel::focusedSelectionIds();
|
return panel::focusedSelectionIds();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,91 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// panel_bank_ops — the bank-CRUD + selection-read seam of the bank panel (Q-W2 split
|
// panel_bank_ops — the bank-CRUD + selection-read seam of the bank panel (Q-W2 split
|
||||||
// of bank_panel.h; Phase B4/B5). The .cpp is the single home of the panel-side bank
|
// of bank_panel.h; Phase B4/B5). The .cpp is the SINGLE implementation home of the
|
||||||
// verbs (create / rename / delete / evacuate / activate / move / copy / remove),
|
// bank verbs (create / rename / delete / evacuate / activate / move / copy / remove):
|
||||||
// each driven against the B1 BankBook model on the session and persisted via
|
// each promptless inner verb below drives the B1 BankBook model on the session and
|
||||||
// persistBankOp (one bank op = one Ctrl-Z) — the owner Q-W4 dedupes actions.cpp
|
// persists via persistBankOp (one bank op = one Ctrl-Z). Q-W4 dedupe: the panel's
|
||||||
// against. This header carries the panel's public selection-read surface.
|
// menu handlers and the bank_actions bindable family are both thin UX skins
|
||||||
|
// (prompts / confirms / console vs. message boxes / panel-state nudges) over these
|
||||||
|
// one-home verbs. This header carries that verb surface, the shared prompt/persist
|
||||||
|
// helpers, and the panel's public selection-read surface.
|
||||||
//
|
//
|
||||||
// REAPER-free: main.cpp (insert action) and actions.cpp read the selection through
|
// The selection reads are REAPER-free; the verbs and helpers are REAPER-facing
|
||||||
// these free functions.
|
// (persist + stock dialogs) but SDK-free in this header.
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
|
// --- Promptless inner bank verbs (Q-W4 single home) --------------------------
|
||||||
|
// Each verb: model op on the session's BankBook + persistBankOp (undo-batched
|
||||||
|
// ext-state persist) — NO prompts, NO message boxes, NO panel-state nudges. The
|
||||||
|
// caller owns all UX. Every verb returns whether the model accepted the mutation
|
||||||
|
// (a rejected op persists nothing and opens no undo point). Verbs resolve the
|
||||||
|
// session via the panel's live session pointer (set at load by bankPanelInit,
|
||||||
|
// before any action can fire) and fail safe (false / "") when it is absent.
|
||||||
|
|
||||||
|
// Mints a stable GUID bank id, creates `name` in the book. Returns the new bank id,
|
||||||
|
// or "" when the model rejects the name (duplicate, trimmed + case-insensitive).
|
||||||
|
// Create is purely organizational — no generation bump.
|
||||||
|
std::string bankOpCreate(const std::string& name);
|
||||||
|
|
||||||
|
// Renames `bankId`. False when the model rejects (pool un-renamable / name in use).
|
||||||
|
bool bankOpRename(const std::string& bankId, const std::string& newName);
|
||||||
|
|
||||||
|
// Deletes `bankId`. False when the model rejects (pool un-deletable). The caller
|
||||||
|
// passes `bumpGeneration` from the member count it read BEFORE any evacuate/delete
|
||||||
|
// (an evacuate-then-delete flow must still bump on the ORIGINAL membership).
|
||||||
|
bool bankOpDelete(const std::string& bankId, bool bumpGeneration);
|
||||||
|
|
||||||
|
// Evacuates `bankId`'s members to the pool. False when the model rejects (the pool
|
||||||
|
// itself). Bumps the generation (membership changed).
|
||||||
|
bool bankOpEvacuate(const std::string& bankId);
|
||||||
|
|
||||||
|
// Activates `bankId` as the capture target. False on an unknown id. No bump.
|
||||||
|
bool bankOpActivate(const std::string& bankId);
|
||||||
|
|
||||||
|
// Moves (copy=false) or copies (copy=true) `sampleIds` from `srcBankId` to
|
||||||
|
// `destBankId` (index-only; files never relocate). Returns whether the index
|
||||||
|
// actually mutated — the verb-aware no-op guardrail: a COPY collapse changes
|
||||||
|
// nothing (no undo point); a MOVE collapse did remove the source entry (counts).
|
||||||
|
// Persists ONE undo point ("move/copy sample(s)") only when mutated.
|
||||||
|
bool bankOpTransfer(const std::vector<std::string>& sampleIds,
|
||||||
|
const std::string& srcBankId, const std::string& destBankId,
|
||||||
|
bool copy);
|
||||||
|
|
||||||
|
// Removes `sampleIds` from `srcBankId` (index-only, this-bank scope; never deletes
|
||||||
|
// bytes). Returns whether anything was removed; persists one undo point when so.
|
||||||
|
bool bankOpRemove(const std::vector<std::string>& sampleIds,
|
||||||
|
const std::string& srcBankId);
|
||||||
|
|
||||||
|
// --- Shared UX/persist helpers ------------------------------------------------
|
||||||
|
|
||||||
|
// Prompts the user for a single line of text via REAPER's stock input dialog
|
||||||
|
// (GetUserInputs). `initial` pre-fills the field. Returns false (leaving `out`
|
||||||
|
// untouched) on cancel or an empty entry. COMMA GUARD: the return separator is
|
||||||
|
// overridden to \x1f (un-typeable) via the documented `separator=X` pseudo-caption,
|
||||||
|
// so any printable name — commas included — round-trips whole (SDK ~3806/3808).
|
||||||
|
// One home (Q-W4) for the former actions.cpp/panel_bank_ops.cpp twins.
|
||||||
|
bool promptText(const char* title, const char* caption, const std::string& initial,
|
||||||
|
std::string& out);
|
||||||
|
|
||||||
|
// Persists a completed bank-index verb as a single REAPER undo point (R-B).
|
||||||
|
// Wraps the session persist (SetProjExtState) in a Begin/End block with
|
||||||
|
// UNDO_STATE_MISCCFG so the bank op is one Ctrl-Z. On an unsaved / no-active project
|
||||||
|
// the persist no-ops and the block is closed with an empty label + zero flag (REAPER
|
||||||
|
// discards it). Callers must invoke this ONLY after a successful/effective mutation —
|
||||||
|
// rejected ops (duplicate name, un-deletable pool, etc.) must return before reaching
|
||||||
|
// here so no empty undo point is ever opened for a no-op.
|
||||||
|
//
|
||||||
|
// S9 bank-generation bump: pass `bumpGeneration = true` for a verb that changes what a
|
||||||
|
// live instance would PLAY — move / copy / remove / evacuate / delete-with-members. Leave
|
||||||
|
// it false (the default) for a PURELY ORGANIZATIONAL verb — create / rename / activate /
|
||||||
|
// reorder. The bump (when requested) happens INSIDE the block, BEFORE the persist, so
|
||||||
|
// the stamped counter rides the same ext-state write and undo captures the pre/post
|
||||||
|
// generation with the rest of the blob.
|
||||||
|
void persistBankOp(const char* label, bool bumpGeneration = false);
|
||||||
|
|
||||||
// The stable ids of the currently-selected samples, in bank (insertion) order.
|
// The stable ids of the currently-selected samples, in bank (insertion) order.
|
||||||
// Empty when nothing is selected or the panel has never opened. This is the clean
|
// Empty when nothing is selected or the panel has never opened. This is the clean
|
||||||
// seam the `insert` action reads to know WHAT to place — it returns ids (not grid
|
// seam the `insert` action reads to know WHAT to place — it returns ids (not grid
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
#include "shell/panel/panel_state.h"
|
#include "shell/panel/panel_state.h"
|
||||||
|
|
||||||
#include "actions.h" // persistBankOp — shared undo-block wrapper (R-B)
|
#include "shell/panel/panel_bank_ops.h" // persistBankOp — shared undo-block wrapper (R-B)
|
||||||
#include "shell/actions/drag_out_win.h" // OLE / SWELL drag-out initiation seam (M11)
|
#include "shell/actions/drag_out_win.h" // OLE / SWELL drag-out initiation seam (M11)
|
||||||
#include "shell/actions/instrument_drop_win.h" // resolveFxDropTarget / performInstrumentDrop (S17)
|
#include "shell/actions/instrument_drop_win.h" // resolveFxDropTarget / performInstrumentDrop (S17)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
#include "shell/panel/panel_state.h"
|
#include "shell/panel/panel_state.h"
|
||||||
#include "shell/panel/panel_input.h"
|
#include "shell/panel/panel_input.h"
|
||||||
|
|
||||||
#include "actions.h" // bankPruneCommandId — the footer Prune dispatch (R3)
|
#include "shell/actions/bank_actions.h" // bankPruneCommandId — the footer Prune dispatch (R3)
|
||||||
#include "persist.h" // ReaSamplerSession — view/tail reads + mutation
|
#include "persist.h" // ReaSamplerSession — view/tail reads + mutation
|
||||||
#include "core/view/view_mode_model.h" // autoTagNewContent / NewItem / AutoTag (D2 Wave 2)
|
#include "core/view/view_mode_model.h" // autoTagNewContent / NewItem / AutoTag (D2 Wave 2)
|
||||||
#include "shell/capture/item_read.h" // itemGuid / itemLaneName — shared item-read seam (D2 W3-B)
|
#include "shell/capture/item_read.h" // itemGuid / itemLaneName — shared item-read seam (D2 W3-B)
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ std::string activeModeIdOrEmpty() {
|
|||||||
|
|
||||||
// The BOTTOM toolbar inventory (L5 refinement 3): FOUR Item/Track x Arrange/Design tag buttons
|
// The BOTTOM toolbar inventory (L5 refinement 3): FOUR Item/Track x Arrange/Design tag buttons
|
||||||
// then a set-apart Show Both. The suffixes are the ACTUAL registered command-id strings from
|
// then a set-apart Show Both. The suffixes are the ACTUAL registered command-id strings from
|
||||||
// actions.cpp (VIEW_MOVE_ITEMS_ARRANGE / VIEW_MOVE_ITEMS_DESIGN for the item moves;
|
// design_view_actions.cpp (VIEW_MOVE_ITEMS_ARRANGE / VIEW_MOVE_ITEMS_DESIGN for the item moves;
|
||||||
// VIEW_TAG_ARRANGE / VIEW_TAG_DESIGN for the track tags; VIEW_SHOW_BOTH) — grepped, not
|
// VIEW_TAG_ARRANGE / VIEW_TAG_DESIGN for the track tags; VIEW_SHOW_BOTH) — grepped, not
|
||||||
// paraphrased. "…: Arrange" routes through the untag/arrange path (Arrange = absence of a tag).
|
// paraphrased. "…: Arrange" routes through the untag/arrange path (Arrange = absence of a tag).
|
||||||
// The Toggle + both Activate buttons are REMOVED (L5 refinement 4 / settled inventory): the
|
// The Toggle + both Activate buttons are REMOVED (L5 refinement 4 / settled inventory): the
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// row/cluster builders, region rects, the L7 slot-order display bridge) is internal to
|
// row/cluster builders, region rects, the L7 slot-order display bridge) is internal to
|
||||||
// panel_layout.cpp (see panel_state.h for the intra-panel seam).
|
// panel_layout.cpp (see panel_state.h for the intra-panel seam).
|
||||||
//
|
//
|
||||||
// REAPER-free: main.cpp / actions.cpp drive these through plain free functions.
|
// REAPER-free: main.cpp / bank_actions.cpp drive these through plain free functions.
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,11 @@
|
|||||||
// * Explicit using-declarations pulling the pure modules' symbols into
|
// * Explicit using-declarations pulling the pure modules' symbols into
|
||||||
// reasampler::panel from their REAL namespace homes (Q-W1 sub-namespaces) — this
|
// reasampler::panel from their REAL namespace homes (Q-W1 sub-namespaces) — this
|
||||||
// header itself does not directly include the interim core/namespaces.h shim
|
// header itself does not directly include the interim core/namespaces.h shim
|
||||||
// (Q-W2 retires that direct dependency for this module). Six of the eight panel
|
// (Q-W2 retires that direct dependency for this module; Q-W4 retired the
|
||||||
// TUs still pull the shim in TRANSITIVELY via actions.h/persist.h/ingest.h/
|
// actions.h carrier with the actions split). Several panel TUs still pull the
|
||||||
// draw_kit.h/view.h; only panel_thumbnails.cpp and panel_audition.cpp are
|
// shim in TRANSITIVELY via persist.h/ingest.h/draw_kit.h/view.h; only
|
||||||
// shim-free end to end. Nothing HERE depends on it either way.
|
// panel_thumbnails.cpp and panel_audition.cpp are shim-free end to end.
|
||||||
|
// Nothing HERE depends on it either way.
|
||||||
//
|
//
|
||||||
// REFERENCE-INVALIDATION GUARDRAIL (CONTEXT.md §Multi-bank): a bank-structural
|
// REFERENCE-INVALIDATION GUARDRAIL (CONTEXT.md §Multi-bank): a bank-structural
|
||||||
// mutation (create/delete/evacuate/activate/move) can reallocate the book's vector,
|
// mutation (create/delete/evacuate/activate/move) can reallocate the book's vector,
|
||||||
|
|||||||
Reference in New Issue
Block a user