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
+78 -18
View File
@@ -1315,6 +1315,55 @@ void transferSamples(const std::vector<std::string>& sampleIds,
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). Confirm-on-last-
// reference guardrail: a single confirm summarizing the N whose files this would orphan
// (computed on the PRE-mutation reference graph), fired only when at least one would
// orphan. Ids passed by value — no BankIndex& cached across the loop's mutations.
void removeSamples(const std::vector<std::string>& sampleIds,
const std::string& srcBankId) {
if (!book() || sampleIds.empty()) return;
const Bank* src = book()->bank(srcBankId);
if (!src) return;
// Count files this remove would orphan — computed BEFORE mutating, so a same-hash
// sibling in another bank counts as a surviving reference.
int orphanCount = 0;
for (const std::string& sid : sampleIds) {
const Sample* s = src->index.query(sid);
if (!s) continue; // already gone; not a last-reference orphan
if (!book()->hashReferencedElsewhere(s->contentHash, srcBankId)) ++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?";
// 4 == MB_YESNO. 6=Yes (SDK); anything else cancels.
const int r = ShowMessageBox(msg.c_str(),
"ReaSampler: remove last-reference sample(s)", 4);
if (r != 6) 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
persistBook();
// 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{};
invalidatePanel();
}
// The selection's sample ids resolved against the FOCUSED region's bank (source of a
// move/copy). Returns ids in bank order; empty when nothing selected.
std::vector<std::string> focusedSelectionIds() {
@@ -1356,6 +1405,7 @@ enum : unsigned int {
kMenuDelete,
kMenuEvacuate,
kMenuCreate,
kMenuRemove, // remove selected sample(s) from the source bank (B5)
kMenuMoveBase = 1000, // move-to-bank: kMenuMoveBase + destination index
kMenuCopyBase = 2000, // copy-to-bank: kMenuCopyBase + destination index
};
@@ -1411,30 +1461,32 @@ void showSelectionMenu(int screenX, int screenY) {
for (const Bank* bk : namedBanks())
if (bk->id != srcId) dests.push_back({bk->id, bk->displayName});
HMENU menu = CreatePopupMenu();
if (dests.empty()) {
menuAppend(menu, kMenuNone, "No other bank to move to", /*grayed=*/true);
TrackPopupMenu(menu, TPM_RETURNCMD, screenX, screenY, 0, g_panel.hwnd, nullptr);
DestroyMenu(menu);
return;
}
const std::string label = std::to_string(sel.size()) +
(sel.size() == 1 ? " sample" : " samples");
menuAppend(menu, kMenuNone, ("Move " + label + " to:").c_str(), /*grayed=*/true);
for (std::size_t i = 0; i < dests.size(); ++i)
menuAppend(menu, kMenuMoveBase + static_cast<unsigned int>(i),
(" " + dests[i].name).c_str());
menuSeparator(menu);
menuAppend(menu, kMenuNone, ("Copy " + label + " to:").c_str(), /*grayed=*/true);
for (std::size_t i = 0; i < dests.size(); ++i)
menuAppend(menu, kMenuCopyBase + static_cast<unsigned int>(i),
(" " + dests[i].name).c_str());
HMENU menu = CreatePopupMenu();
// Move/copy blocks appear only when there is another bank to transfer to; Remove is
// always offered (it needs no destination — it drops the entry from the source).
if (!dests.empty()) {
menuAppend(menu, kMenuNone, ("Move " + label + " to:").c_str(), /*grayed=*/true);
for (std::size_t i = 0; i < dests.size(); ++i)
menuAppend(menu, kMenuMoveBase + static_cast<unsigned int>(i),
(" " + dests[i].name).c_str());
menuSeparator(menu);
menuAppend(menu, kMenuNone, ("Copy " + label + " to:").c_str(), /*grayed=*/true);
for (std::size_t i = 0; i < dests.size(); ++i)
menuAppend(menu, kMenuCopyBase + static_cast<unsigned int>(i),
(" " + dests[i].name).c_str());
menuSeparator(menu);
}
menuAppend(menu, kMenuRemove, ("Remove " + label + "\xE2\x80\xA6").c_str());
const int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, screenX, screenY, 0,
g_panel.hwnd, nullptr);
DestroyMenu(menu);
if (cmd >= static_cast<int>(kMenuMoveBase) &&
if (cmd == static_cast<int>(kMenuRemove)) {
removeSamples(sel, srcId);
} else if (cmd >= static_cast<int>(kMenuMoveBase) &&
cmd < static_cast<int>(kMenuMoveBase + dests.size())) {
transferSamples(sel, srcId, dests[cmd - kMenuMoveBase].id, /*copy=*/false);
} else if (cmd >= static_cast<int>(kMenuCopyBase) &&
@@ -1670,6 +1722,14 @@ bool handleKey(int vk) {
case VK_ESCAPE:
stopAudition();
return true;
case VK_DELETE: {
// Remove the focused-region selection (B5). Same confirm-on-last-reference
// path the context-menu "Remove" uses; a no-op when nothing is selected.
const std::vector<std::string> sel = focusedSelectionIds();
if (sel.empty()) return false; // nothing selected — let the key fall through
removeSamples(sel, bankIdForRegion(g_panel.focusedRegion));
return true;
}
default:
return false;
}