|
|
|
@@ -1,13 +1,15 @@
|
|
|
|
|
// 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
|
|
|
|
|
// (create / rename / delete / evacuate / activate / move / copy / remove) — the owner
|
|
|
|
|
// Q-W4 dedupes actions.cpp against — plus the book/bank accessors, the popup menus
|
|
|
|
|
// that drive them, and the selection-id / OS-drag path resolvers.
|
|
|
|
|
// split of bank_panel.cpp; Phase B4/B5). Since Q-W4 this TU is the ONE implementation
|
|
|
|
|
// home of the bank verbs (create / rename / delete / evacuate / activate / move /
|
|
|
|
|
// copy / remove): the promptless bankOp* inner verbs (model op + persistBankOp only)
|
|
|
|
|
// 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 =
|
|
|
|
|
// one Ctrl-Z; a true index no-op opens NO undo point). It DOES mutate the bank BOOK —
|
|
|
|
|
// that is the whole point of B4 — but only the index/model + ext-state, never the
|
|
|
|
|
// arrange, never a sample file on disk (bank ops are index-only; files stay put —
|
|
|
|
|
// Each verb mutates the session's book() then persists via persistBankOp() (one bank
|
|
|
|
|
// op = one Ctrl-Z; a true index no-op opens NO undo point). It DOES mutate the bank
|
|
|
|
|
// BOOK — that is the whole point of B4 — but only the index/model + ext-state, never
|
|
|
|
|
// 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
|
|
|
|
|
// mutation any Bank*/BankModel& is invalid — resolve fresh, pass ids.
|
|
|
|
|
//
|
|
|
|
@@ -23,7 +25,6 @@
|
|
|
|
|
#include "shell/panel/panel_state.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
|
|
|
|
|
|
|
|
|
|
#define REAPERAPI_MINIMAL
|
|
|
|
@@ -33,6 +34,8 @@
|
|
|
|
|
#define REAPERAPI_WANT_Main_OnCommand
|
|
|
|
|
#define REAPERAPI_WANT_genGuid
|
|
|
|
|
#define REAPERAPI_WANT_guidToString
|
|
|
|
|
#define REAPERAPI_WANT_Undo_BeginBlock2
|
|
|
|
|
#define REAPERAPI_WANT_Undo_EndBlock2
|
|
|
|
|
#include "reaper_plugin_functions.h"
|
|
|
|
|
|
|
|
|
|
namespace reasampler::panel {
|
|
|
|
@@ -79,55 +82,28 @@ std::vector<const Bank*> namedBanks() {
|
|
|
|
|
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
|
|
|
|
|
// STRUCTURAL 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
|
|
|
|
|
// Q-W4: each handler here owns only the panel's UX (prompts / confirms / message
|
|
|
|
|
// boxes / panel-state nudges / repaint); the model op + persist is the shared
|
|
|
|
|
// bankOp* inner verb (defined in the public section below). After a STRUCTURAL
|
|
|
|
|
// 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).
|
|
|
|
|
|
|
|
|
|
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() {
|
|
|
|
|
if (!book()) return;
|
|
|
|
|
std::string name;
|
|
|
|
|
if (!promptText("ReaSampler: create bank", "Bank name:", "", name)) return;
|
|
|
|
|
const std::string id = mintBankId();
|
|
|
|
|
if (!book()->createBank(id, name)) {
|
|
|
|
|
const std::string id = bankOpCreate(name);
|
|
|
|
|
if (id.empty()) {
|
|
|
|
|
ShowMessageBox("A bank with that name already exists.",
|
|
|
|
|
"ReaSampler: create bank", 0);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
g_panel.shownBankId = id; // show the freshly-created bank
|
|
|
|
|
g_panel.focusedRegion = Region::Banks;
|
|
|
|
|
persistBankOp("ReaSampler: create bank");
|
|
|
|
|
invalidatePanel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -140,12 +116,11 @@ void doRenameBank(const std::string& bankId) {
|
|
|
|
|
const std::string current = bk->displayName; // copy before any mutation
|
|
|
|
|
std::string newName;
|
|
|
|
|
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.",
|
|
|
|
|
"ReaSampler: rename bank", 0);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
persistBankOp("ReaSampler: rename bank");
|
|
|
|
|
invalidatePanel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -177,11 +152,11 @@ void doDeleteBank(const std::string& bankId) {
|
|
|
|
|
}
|
|
|
|
|
// 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-
|
|
|
|
|
// delete path moved/dropped members) — both change what a live instance could play. An
|
|
|
|
|
// empty-bank delete is purely organizational, no bump.
|
|
|
|
|
persistBankOp("ReaSampler: delete bank", /*bumpGeneration=*/members > 0);
|
|
|
|
|
// empty-bank delete is purely organizational, no bump. The ORIGINAL member count decides
|
|
|
|
|
// (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,
|
|
|
|
|
// nudge focus to the pool so the selection has a valid home.
|
|
|
|
|
if (namedBanks().empty()) g_panel.focusedRegion = Region::Pool;
|
|
|
|
@@ -192,79 +167,39 @@ void doEvacuateBank(const std::string& bankId) {
|
|
|
|
|
if (!book()) return;
|
|
|
|
|
const Bank* bk = book()->bank(bankId);
|
|
|
|
|
if (!bk || bk->isPool()) return;
|
|
|
|
|
if (!book()->evacuate(bankId)) return;
|
|
|
|
|
persistBankOp("ReaSampler: evacuate bank", /*bumpGeneration=*/true); // S9: membership changed
|
|
|
|
|
if (!bankOpEvacuate(bankId)) return;
|
|
|
|
|
invalidatePanel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void doActivateBank(const std::string& bankId) {
|
|
|
|
|
if (!book()) return;
|
|
|
|
|
if (!book()->setActiveBank(bankId)) return; // rejects an unknown id
|
|
|
|
|
persistBankOp("ReaSampler: activate bank");
|
|
|
|
|
if (!bankOpActivate(bankId)) return; // rejects an unknown id
|
|
|
|
|
invalidatePanel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
// Move or copy `sampleIds` from `srcBankId` to `destBankId` (index-only). Both pass
|
|
|
|
|
// ids straight to the model op (no BankModel& cached across the loop's mutations).
|
|
|
|
|
//
|
|
|
|
|
// NO-OP GUARDRAIL — VERB-AWARE (matches the action layer's doBankTransferSelected):
|
|
|
|
|
// * 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.
|
|
|
|
|
// Move or copy `sampleIds` from `srcBankId` to `destBankId` (index-only). Thin panel
|
|
|
|
|
// 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
|
|
|
|
|
// and repaints on an actual mutation.
|
|
|
|
|
void transferSamples(const std::vector<std::string>& sampleIds,
|
|
|
|
|
const std::string& srcBankId, const std::string& destBankId,
|
|
|
|
|
bool copy) {
|
|
|
|
|
if (!book()) return;
|
|
|
|
|
if (sampleIds.empty() || srcBankId == destBankId) return;
|
|
|
|
|
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
|
|
|
|
|
if (!bankOpTransfer(sampleIds, srcBankId, destBankId, copy))
|
|
|
|
|
return; // nothing changed — no persist, no undo point
|
|
|
|
|
// 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).
|
|
|
|
|
g_panel.selection = Selection{};
|
|
|
|
|
invalidatePanel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove `sampleIds` from `srcBankId` (index-only, this-bank scope). 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). Removes are silent
|
|
|
|
|
// (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.
|
|
|
|
|
// Remove `sampleIds` from `srcBankId` (index-only, this-bank scope). Thin panel skin
|
|
|
|
|
// over bankOpRemove — see the verb for the never-deletes-bytes / silent-remove /
|
|
|
|
|
// one-Ctrl-Z contract. Clears the stale selection and repaints on an actual removal.
|
|
|
|
|
void removeSamples(const std::vector<std::string>& sampleIds,
|
|
|
|
|
const std::string& srcBankId) {
|
|
|
|
|
if (!book() || sampleIds.empty()) return;
|
|
|
|
|
if (!book()->bank(srcBankId)) return;
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
if (!bankOpRemove(sampleIds, srcBankId))
|
|
|
|
|
return; // nothing changed — no persist, no undo point
|
|
|
|
|
// 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).
|
|
|
|
|
g_panel.selection = Selection{};
|
|
|
|
@@ -471,10 +406,189 @@ void showSelectionMenu(int screenX, int screenY) {
|
|
|
|
|
|
|
|
|
|
} // namespace reasampler::panel
|
|
|
|
|
|
|
|
|
|
// --- Public API (the selection read seam — panel_bank_ops.h) -------------------
|
|
|
|
|
// --- Public API (panel_bank_ops.h) ---------------------------------------------
|
|
|
|
|
|
|
|
|
|
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() {
|
|
|
|
|
return panel::focusedSelectionIds();
|
|
|
|
|
}
|
|
|
|
|