Q-W2: split bank_panel.cpp (3459 LOC) into eight shell/panel TUs — reasampler::panel internals, per-seam public headers, shim retired; zero behavior change, 60/60 green
This commit is contained in:
@@ -0,0 +1,478 @@
|
||||
// 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.
|
||||
//
|
||||
// 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 —
|
||||
// CONTEXT.md §Multi-bank). REFERENCE-INVALIDATION GUARDRAIL: after a STRUCTURAL
|
||||
// mutation any Bank*/BankModel& is invalid — resolve fresh, pass ids.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
|
||||
// extern (CLAUDE.md §contract). DAW-verified, not unit tested.
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_GetUserInputs
|
||||
#define REAPERAPI_WANT_ShowMessageBox
|
||||
#define REAPERAPI_WANT_Main_OnCommand
|
||||
#define REAPERAPI_WANT_genGuid
|
||||
#define REAPERAPI_WANT_guidToString
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler::panel {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// --- Current-project directory (mirrors persist.cpp's derivation) -------------
|
||||
std::string currentProjectDir() {
|
||||
std::vector<char> buf(4096, '\0');
|
||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
std::string rpp(buf.data());
|
||||
if (rpp.empty()) return {};
|
||||
return normalizeSlashes(fs::path(rpp).parent_path().string());
|
||||
}
|
||||
|
||||
// --- Book / bank accessors ----------------------------------------------------
|
||||
|
||||
BankBook* book() { return g_panel.session ? &g_panel.session->book() : nullptr; }
|
||||
|
||||
// The BankModel a region currently displays. Pool region -> the pool; banks region ->
|
||||
// the shown tab's bank (or nullptr when no named banks / the id went stale). Resolved
|
||||
// FRESH every call (never cached across a mutation).
|
||||
const BankModel* indexForRegion(Region r) {
|
||||
BankBook* b = book();
|
||||
if (!b) return nullptr;
|
||||
if (r == Region::Pool) return &b->pool().index;
|
||||
if (g_panel.shownBankId.empty()) return nullptr;
|
||||
return b->index(g_panel.shownBankId);
|
||||
}
|
||||
|
||||
// The bank id a region displays (pool id, or the shown tab's id; "" when none).
|
||||
std::string bankIdForRegion(Region r) {
|
||||
if (r == Region::Pool) return std::string(kPoolBankId);
|
||||
return g_panel.shownBankId;
|
||||
}
|
||||
|
||||
// The named banks in ordinal order (pool excluded) — the tabs. Resolved fresh.
|
||||
std::vector<const Bank*> namedBanks() {
|
||||
std::vector<const Bank*> out;
|
||||
BankBook* b = book();
|
||||
if (!b) return out;
|
||||
for (const Bank& bk : b->banks())
|
||||
if (!bk.isPool()) out.push_back(&bk);
|
||||
return out;
|
||||
}
|
||||
|
||||
// --- Bank management ops (id-keyed; drive the B1 model + persist) --------------
|
||||
//
|
||||
// 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
|
||||
// survives (matches the capture/B3 quiet-persist idiom).
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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)) {
|
||||
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();
|
||||
}
|
||||
|
||||
void doRenameBank(const std::string& bankId) {
|
||||
if (!book()) return;
|
||||
const Bank* bk = book()->bank(bankId);
|
||||
if (!bk || bk->isPool()) return;
|
||||
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)) {
|
||||
ShowMessageBox("Another bank already uses that name.",
|
||||
"ReaSampler: rename bank", 0);
|
||||
return;
|
||||
}
|
||||
persistBankOp("ReaSampler: rename bank");
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
// Delete with the RICHER confirm-on-non-empty affordance (B4): the confirm names the
|
||||
// member count AND offers evacuate as the one-click alternative (Yes=delete anyway,
|
||||
// No=evacuate-then-keep, Cancel=abort) — richer than B3's basic YESNO.
|
||||
void doDeleteBank(const std::string& bankId) {
|
||||
if (!book()) return;
|
||||
const Bank* bk = book()->bank(bankId);
|
||||
if (!bk || bk->isPool()) return;
|
||||
const std::size_t members = bk->index.size(); // read BEFORE any mutation
|
||||
const std::string name = bk->displayName;
|
||||
|
||||
if (members > 0) {
|
||||
const std::string msg =
|
||||
"\"" + name + "\" holds " + std::to_string(members) +
|
||||
(members == 1 ? " sample" : " samples") +
|
||||
".\n\nYes -- delete the bank AND drop its samples (files are kept on disk "
|
||||
"but no bank references them until prune).\nNo -- Evacuate them to the "
|
||||
"pool first, then delete the empty bank (keeps the samples).\nCancel -- "
|
||||
"do nothing.";
|
||||
// 3 == MB_YESNOCANCEL. 6=Yes, 7=No, 2=Cancel (SDK).
|
||||
const int r = ShowMessageBox(msg.c_str(),
|
||||
"ReaSampler: delete non-empty bank", 3);
|
||||
if (r == 2) return; // Cancel
|
||||
if (r == 7) { // No -> evacuate, then delete empty
|
||||
if (!book()->evacuate(bankId)) return;
|
||||
// book() may have reallocated; re-resolve nothing (we pass the id again).
|
||||
}
|
||||
// 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);
|
||||
// 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;
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
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
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
void doActivateBank(const std::string& bankId) {
|
||||
if (!book()) return;
|
||||
if (!book()->setActiveBank(bankId)) return; // rejects an unknown id
|
||||
persistBankOp("ReaSampler: activate bank");
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
// 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.
|
||||
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
|
||||
// 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.
|
||||
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
|
||||
// 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() {
|
||||
// L7: selection ordinals index the DISPLAY (slot) order, not BankModel insertion order.
|
||||
// orderedIds[i] is the id at selection ordinal i.
|
||||
std::vector<std::string> ids;
|
||||
const RegionDisplay disp = focusedDisplay();
|
||||
const int count = disp.occupiedCount();
|
||||
for (int i : g_panel.selection.indices)
|
||||
if (i >= 0 && i < count) ids.push_back(disp.orderedIds[static_cast<std::size_t>(i)]);
|
||||
return ids;
|
||||
}
|
||||
|
||||
// Resolves the ARMED drag payload (g_panel.dragSampleIds, from g_panel.dragSourceBankId) to
|
||||
// the absolute, existing-file path list for a native OS drag-out (M11). Reuses the SAME M4
|
||||
// path machinery the panel uses for audition/insert (resolveBankFile over the current
|
||||
// project dir) — no temp copies; the drag points straight at the on-disk bank files. Each
|
||||
// id is looked up in its SOURCE bank's index (the payload's origin, not the focused region,
|
||||
// which can differ once the pointer roams), resolved, stat'd, then handed to the pure
|
||||
// drag_out::assemblePathList for dedupe + skip-missing/unresolved policy. Read-only: no
|
||||
// mutation of sample / index / selection (invariant #2).
|
||||
std::vector<std::string> resolveDragPathsForOs() {
|
||||
std::vector<ResolvedSample> resolved;
|
||||
BankBook* b = book();
|
||||
if (!b) return {};
|
||||
const BankModel* idx = b->index(g_panel.dragSourceBankId);
|
||||
if (!idx) return {};
|
||||
|
||||
const std::string projectDir = currentProjectDir();
|
||||
resolved.reserve(g_panel.dragSampleIds.size());
|
||||
for (const std::string& sid : g_panel.dragSampleIds) {
|
||||
const Sample* s = idx->query(sid);
|
||||
if (!s) continue; // stale id — the pure layer would skip it anyway; nothing to resolve
|
||||
ResolvedSample rs;
|
||||
rs.absolutePath = resolveBankFile(projectDir, s->relativePath);
|
||||
rs.fileExists = !rs.absolutePath.empty() && fs::exists(fs::path(rs.absolutePath));
|
||||
resolved.push_back(std::move(rs));
|
||||
}
|
||||
return assemblePathList(resolved).paths;
|
||||
}
|
||||
|
||||
// --- Popup menus --------------------------------------------------------------
|
||||
//
|
||||
// SWELL/Win32 both expose CreatePopupMenu / InsertMenu (SWELL aliases SWELL_InsertMenu
|
||||
// -> InsertMenu) / TrackPopupMenu(TPM_RETURNCMD) / DestroyMenu. We build a menu of
|
||||
// (label -> small int command), track it at screen coords, and switch on the return.
|
||||
// Menu command ids are LOCAL to the popup (not REAPER action ids) — TPM_RETURNCMD
|
||||
// hands the chosen id straight back, so no hookcommand routing is involved.
|
||||
|
||||
// Appends a string item (id) to `menu` at its end. Portable over Win32/SWELL: both
|
||||
// accept InsertMenu(menu, pos, MF_BYPOSITION|MF_STRING, id, text) with a negative
|
||||
// position appending. Win32 and SWELL both treat pos < 0 as an append.
|
||||
void menuAppend(HMENU menu, unsigned int id, const char* text, bool grayed = false) {
|
||||
UINT flags = MF_BYPOSITION | MF_STRING;
|
||||
if (grayed) flags |= MF_GRAYED;
|
||||
InsertMenu(menu, -1, flags, id, text);
|
||||
}
|
||||
void menuSeparator(HMENU menu) {
|
||||
InsertMenu(menu, -1, MF_BYPOSITION | MF_SEPARATOR, 0, nullptr);
|
||||
}
|
||||
|
||||
// Menu command ids (local to a popup).
|
||||
enum : unsigned int {
|
||||
kMenuNone = 0,
|
||||
kMenuActivate = 100,
|
||||
kMenuRename,
|
||||
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
|
||||
};
|
||||
|
||||
// Shows the right-click context menu for a named-bank TAB: activate / rename / delete
|
||||
// / evacuate that bank, plus a create entry. Drives the id-keyed ops.
|
||||
void showTabMenu(int screenX, int screenY, const std::string& bankId) {
|
||||
if (!book()) return;
|
||||
const Bank* bk = book()->bank(bankId);
|
||||
if (!bk || bk->isPool()) return;
|
||||
const bool isActive = book()->activeBankId() == bankId;
|
||||
const bool nonEmpty = !bk->index.empty();
|
||||
|
||||
HMENU menu = CreatePopupMenu();
|
||||
menuAppend(menu, kMenuActivate,
|
||||
isActive ? "Active (capture target)" : "Activate (make capture target)",
|
||||
/*grayed=*/isActive);
|
||||
menuSeparator(menu);
|
||||
menuAppend(menu, kMenuRename, "Rename...");
|
||||
menuAppend(menu, kMenuEvacuate, "Evacuate to pool", /*grayed=*/!nonEmpty);
|
||||
menuAppend(menu, kMenuDelete, "Delete...");
|
||||
menuSeparator(menu);
|
||||
menuAppend(menu, kMenuCreate, "New bank...");
|
||||
|
||||
const int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, screenX, screenY, 0,
|
||||
g_panel.hwnd, nullptr);
|
||||
DestroyMenu(menu);
|
||||
|
||||
switch (cmd) {
|
||||
case kMenuActivate: doActivateBank(bankId); break;
|
||||
case kMenuRename: doRenameBank(bankId); break;
|
||||
case kMenuEvacuate: doEvacuateBank(bankId); break;
|
||||
case kMenuDelete: doDeleteBank(bankId); break;
|
||||
case kMenuCreate: doCreateBank(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// Opens the top-toolbar overflow ("⋯" More) popup at the button's screen position and fires the
|
||||
// chosen rare-capture variant's command (L5 refinement 1). Menu ids are LOCAL to the popup
|
||||
// (1-based ordinal into overflowMenuRows); TPM_RETURNCMD hands the chosen id back, then we
|
||||
// resolve + fire the corresponding registered command id via the SAME contract the visible
|
||||
// buttons use. Defined here (after menuAppend/menuSeparator); forward-declared above.
|
||||
void showMoreMenu() {
|
||||
if (!g_panel.hwnd) return;
|
||||
const std::vector<ActionBarRow> rows = overflowMenuRows();
|
||||
if (rows.empty()) return;
|
||||
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const MenuButtonRect mb = topMenuButtonRect(cr.right - cr.left);
|
||||
if (mb.empty()) return;
|
||||
|
||||
HMENU menu = CreatePopupMenu();
|
||||
for (std::size_t i = 0; i < rows.size(); ++i) {
|
||||
const int cmd = resolveBarCommandId(rows[i]);
|
||||
// Grey a variant not registered on this channel (defensive — all three are registered).
|
||||
menuAppend(menu, static_cast<unsigned int>(i + 1), rows[i].fullName.c_str(),
|
||||
/*grayed=*/cmd == 0);
|
||||
}
|
||||
|
||||
// Anchor the popup at the button's bottom-left, in screen coords.
|
||||
POINT pt{mb.x, mb.y + mb.height};
|
||||
ClientToScreen(g_panel.hwnd, &pt);
|
||||
const int chosen = TrackPopupMenu(menu, TPM_RETURNCMD, pt.x, pt.y, 0, g_panel.hwnd, nullptr);
|
||||
DestroyMenu(menu);
|
||||
|
||||
if (chosen >= 1 && chosen <= static_cast<int>(rows.size())) {
|
||||
const int cmd = resolveBarCommandId(rows[static_cast<std::size_t>(chosen - 1)]);
|
||||
if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Shows the move/copy menu for the current selection (the SOURCE is the focused
|
||||
// region's bank). Lists every OTHER bank (pool + named) as a move destination, then a
|
||||
// copy submenu-free flat list (copy entries follow the move block). Move is the
|
||||
// default (listed first); copy is the deliberate secondary act.
|
||||
void showSelectionMenu(int screenX, int screenY) {
|
||||
const std::vector<std::string> sel = focusedSelectionIds();
|
||||
if (sel.empty()) return;
|
||||
const std::string srcId = bankIdForRegion(g_panel.focusedRegion);
|
||||
|
||||
// Destinations: pool + named banks, excluding the source. Ordinal order.
|
||||
struct Dest { std::string id; std::string name; };
|
||||
std::vector<Dest> dests;
|
||||
if (srcId != std::string(kPoolBankId))
|
||||
dests.push_back({std::string(kPoolBankId), std::string(kPoolBankName)});
|
||||
for (const Bank* bk : namedBanks())
|
||||
if (bk->id != srcId) dests.push_back({bk->id, bk->displayName});
|
||||
|
||||
const std::string label = std::to_string(sel.size()) +
|
||||
(sel.size() == 1 ? " sample" : " samples");
|
||||
|
||||
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 + "...").c_str());
|
||||
|
||||
const int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, screenX, screenY, 0,
|
||||
g_panel.hwnd, nullptr);
|
||||
DestroyMenu(menu);
|
||||
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) &&
|
||||
cmd < static_cast<int>(kMenuCopyBase + dests.size())) {
|
||||
transferSamples(sel, srcId, dests[cmd - kMenuCopyBase].id, /*copy=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace reasampler::panel
|
||||
|
||||
// --- Public API (the selection read seam — panel_bank_ops.h) -------------------
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
std::vector<std::string> bankPanelSelectedSampleIds() {
|
||||
return panel::focusedSelectionIds();
|
||||
}
|
||||
|
||||
std::string bankPanelSelectedSourceBankId() {
|
||||
// The focused region's displayed bank is the move/copy source. Default to the
|
||||
// pool (a safe source) when nothing is selected / the panel never opened.
|
||||
if (panel::g_panel.selection.empty()) return std::string(kPoolBankId);
|
||||
const std::string id = panel::bankIdForRegion(panel::g_panel.focusedRegion);
|
||||
return id.empty() ? std::string(kPoolBankId) : id;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user