B5: sample-remove verb — index-only drop, this-bank scope, confirm-on-last-reference

This commit is contained in:
2026-07-26 15:07:41 -04:00
parent 22f07d0654
commit 6c1efa0f25
5 changed files with 373 additions and 18 deletions
+83
View File
@@ -420,6 +420,7 @@ constexpr const char* kIdBankActivateNext = "CEREBELLUM_REASAMPLER_BANK_ACTIVATE
constexpr const char* kIdBankActivatePool = "CEREBELLUM_REASAMPLER_BANK_ACTIVATE_POOL";
constexpr const char* kIdBankMoveSel = "CEREBELLUM_REASAMPLER_BANK_MOVE_SELECTED";
constexpr const char* kIdBankCopySel = "CEREBELLUM_REASAMPLER_BANK_COPY_SELECTED";
constexpr const char* kIdBankRemoveSel = "CEREBELLUM_REASAMPLER_BANK_REMOVE_SELECTED";
constexpr const char* kIdBankPoolFull = "CEREBELLUM_REASAMPLER_BANK_POOL_FULLHEIGHT";
constexpr const char* kIdBankBanksFull = "CEREBELLUM_REASAMPLER_BANK_BANKS_FULLHEIGHT";
@@ -431,6 +432,7 @@ 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;
@@ -442,6 +444,7 @@ 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{};
@@ -747,6 +750,81 @@ void doBankTransferSelected(bool copy) {
ShowConsoleMsg(log.c_str());
}
// Remove the panel's selected samples from the SOURCE bank (the focused region's
// displayed bank — bankPanelSelectedSourceBankId, same source as move/copy). Index-only
// and 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).
//
// SCOPE (fork R-A): this-bank only — the sole surfaced verb. The RemoveScope::AllBanks
// seam stays latent in the model; nothing here reaches for it.
//
// CONFIRM-ON-LAST-REFERENCE (guardrail): a remove that would orphan a file (no OTHER
// bank references its content hash after the remove) earns a confirm; a remove of a
// still-referenced sample does not. BATCH UX: for a multi-select we compute the
// last-reference set BEFORE mutating (removal changes the reference graph), then fire a
// SINGLE confirm summarizing the N that would orphan — not one dialog per sample. If
// none would orphan, no confirm fires at all (the confirm is earned by actual risk).
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();
BankBook& book = g_session->book();
const Bank* src = book.bank(srcId);
if (src == nullptr) {
ShowConsoleMsg("ReaSampler: the selection's bank no longer exists.\n");
return;
}
// Count the samples whose file this remove would orphan — computed on the CURRENT
// (pre-mutation) reference graph so a same-hash sibling in another bank counts as a
// surviving reference. Resolve by id against the live source index (ids, not cached
// refs); an id no longer present is skipped (it removes to a no-op below).
int orphanCount = 0;
for (const std::string& sampleId : selected) {
const Sample* s = src->index.query(sampleId);
if (s == nullptr) continue; // already gone; not a last-reference orphan
if (!book.hashReferencedElsewhere(s->contentHash, srcId)) ++orphanCount;
}
if (orphanCount > 0) {
const std::string msg =
std::to_string(orphanCount) +
(orphanCount == 1 ? " selected sample is" : " selected samples are") +
" in no other bank.\n\nRemoving " +
(orphanCount == 1 ? "it" : "them") +
" drops the index entry only — the file stays on disk until you prune "
"(it is never deleted by remove).\n\nRemove anyway?";
const int r = ShowMessageBox(msg.c_str(),
"ReaSampler: remove last-reference sample(s)", 4);
if (r != 6) return; // 6 == YES; anything else cancels (SDK ~6544)
}
// Perform the removes (this-bank scope). Pass ids by value — no BankIndex& is cached
// across the loop's mutations. Count real drops so the no-op guardrail can skip the
// undo point when nothing was removed (every id was already absent).
int removed = 0, absent = 0;
for (const std::string& sampleId : selected) {
switch (book.removeSample(sampleId, srcId, RemoveScope::ThisBank)) {
case RemoveResult::Removed: ++removed; break;
case RemoveResult::RejectedSampleAbsent: ++absent; break;
// Unknown bank cannot occur — srcId was resolved to a live bank above.
case RemoveResult::RejectedUnknownBank: break;
}
}
// No-op guardrail (R-B): open an undo point only if the index actually mutated.
if (removed > 0) persistBankOp("ReaSampler: remove sample(s)");
std::string log = "ReaSampler: removed " + std::to_string(removed) +
(removed == 1 ? " sample" : " samples");
if (absent) log += ", " + std::to_string(absent) + " no longer present";
log += ".\n";
ShowConsoleMsg(log.c_str());
}
} // namespace
void bankRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) {
@@ -768,6 +846,8 @@ void bankRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session)
"ReaSampler: move selected samples to bank");
g_cmdBankCopySel = registerAction(rec, kIdBankCopySel, g_accelBankCopySel,
"ReaSampler: copy selected samples to bank");
g_cmdBankRemoveSel = registerAction(rec, kIdBankRemoveSel, g_accelBankRemoveSel,
"ReaSampler: remove selected samples");
g_cmdBankPoolFull = registerAction(rec, kIdBankPoolFull, g_accelBankPoolFull,
"ReaSampler: toggle pool full-height");
g_cmdBankBanksFull = registerAction(rec, kIdBankBanksFull, g_accelBankBanksFull,
@@ -785,6 +865,7 @@ bool bankHandleCommand(int command) {
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; }
@@ -797,6 +878,8 @@ void bankUnregisterActions(reaper_plugin_info_t* rec) {
rec->Register("-command_id", (void*)kIdBankBanksFull);
rec->Register("-gaccel", (void*)&g_accelBankPoolFull);
rec->Register("-command_id", (void*)kIdBankPoolFull);
rec->Register("-gaccel", (void*)&g_accelBankRemoveSel);
rec->Register("-command_id", (void*)kIdBankRemoveSel);
rec->Register("-gaccel", (void*)&g_accelBankCopySel);
rec->Register("-command_id", (void*)kIdBankCopySel);
rec->Register("-gaccel", (void*)&g_accelBankMoveSel);