import: a .rsbank lands as a new bank, whole or not at all

Four collisions answered explicitly: ids reminted, names never overwritten,
content deduped before the write, bank name auto-suffixed. Degraded ledger
refuses before the picker.
This commit is contained in:
2026-08-02 13:21:48 -04:00
parent 33ea95078d
commit a927dad2f4
26 changed files with 1689 additions and 24 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ live in `shell/bank_ops`, a sibling directory, not here.
## Modules
- `bank_panel` (`shell/panel/`: `panel_window` / `panel_layout` / `panel_render` / `panel_input` / `panel_drag` / `panel_thumbnails` / `panel_audition` / `panel_bank_ops`, sharing state via `panel_state.h` — Q-W2 split of the former god-module into eight TUs) — docked LICE-drawn grid with three-zone layout: top toolbar (Capture → Maintenance → Placement via `action_bar`, short labels, More (⋯) overflow menu via `overflow_menu`), bottom toolbar (four opposite-mode tag buttons + Show Both), and footer (`[Arrange|Design]` toggle, Tail button, Prune via `footer_bar`). Grid renders in sparse slot order with gap cells, drop dispatch, metadata overlay, and selection via `accent/tertiary` purple border. Draws through the L1 kit by palette role; OS drag-out via `drag_out` + `drag_out_win`. `panel_window` owns the SWELL dialog lifecycle + dialog proc + drop-target opt-in; `panel_layout` the toolbar/footer/menu rects + vertical-split geometry (the one geometry source both paint and hit-test read); `panel_render` the WM_PAINT draw; `panel_input` click/wheel/keyboard routing + the new-content auto-tag timer; `panel_drag` the hover + card-drag state machine + drop dispatch; `panel_thumbnails` the PCM→envelope thumbnail cache + the bank-change fingerprint pass; `panel_audition` the preview-playback engine; `panel_bank_ops` the menu/prompt UX skin over the promptless `shell/bank_ops` verbs. `draw_kit` (shared with the VST3 editor) stays a separate TU.
- `panel_window` — SWELL dialog lifecycle + dialog proc + drop-target opt-in.
- `panel_window` — SWELL dialog lifecycle + dialog proc + drop-target opt-in. The drop splits by extension: a `.rsbank` is a whole bank and routes to the package-import action (one NEW bank each), everything else keeps the audio-ingest route.
- `panel_layout` — toolbar/footer/menu rects + vertical-split geometry (the one geometry source both paint and hit-test read).
- `panel_render` — the WM_PAINT draw.
- `panel_input` — click/wheel/keyboard routing + the new-content auto-tag timer.
+8
View File
@@ -16,6 +16,7 @@
#include "shell/panel/panel_state.h"
#include "shell/panel/panel_bank_ops.h"
#include "shell/actions/package_import_action.h" // doImportBankPackage — the menu's import row
#include "shell/bank_ops/bank_ops.h" // bankOp* promptless verbs
#include "shell/persist/session.h" // ReaSamplerSession — the live session the ops mutate
@@ -242,6 +243,7 @@ enum : unsigned int {
kMenuEvacuate,
kMenuCreate,
kMenuRemove, // remove selected sample(s) from the source bank
kMenuImportPackage, // land a .rsbank as a NEW bank (never merges into this one)
kMenuMoveBase = 1000, // move-to-bank: kMenuMoveBase + destination index
kMenuCopyBase = 2000, // copy-to-bank: kMenuCopyBase + destination index
};
@@ -267,6 +269,7 @@ void showTabMenu(int screenX, int screenY, const std::string& bankId) {
menuAppend(menu, kMenuDelete, "Delete...");
menuSeparator(menu);
menuAppend(menu, kMenuCreate, "New bank...");
menuAppend(menu, kMenuImportPackage, "Import bank package...");
const int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, screenX, screenY, 0,
g_panel.hwnd, nullptr);
@@ -278,6 +281,11 @@ void showTabMenu(int screenX, int screenY, const std::string& bankId) {
case kMenuEvacuate: doEvacuateBank(bankId); break;
case kMenuDelete: doDeleteBank(bankId); break;
case kMenuCreate: doCreateBank(); break;
// Always a NEW bank, never a merge into the right-clicked one — the row sits
// here because this is the panel's bank menu, not because it targets this bank.
case kMenuImportPackage:
if (g_panel.session) doImportBankPackage(*g_panel.session);
break;
default: break;
}
}
+21 -3
View File
@@ -15,6 +15,7 @@
#include "shell/panel/draw_kit.h"
#include "shell/actions/ingest.h"
#include "shell/actions/package_import_action.h"
#ifdef _WIN32
#include <windowsx.h> // GET_X_LPARAM / GET_Y_LPARAM (SWELL supplies them on mac/linux)
@@ -40,12 +41,24 @@ PanelState g_panel;
namespace {
bool isPackagePath(const std::string& path) {
static const std::string kExt = ".rsbank";
if (path.size() <= kExt.size()) return false;
std::string tail = path.substr(path.size() - kExt.size());
for (char& c : tail)
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
return tail == kExt;
}
// DragQueryFile(hDrop, 0xFFFFFFFF, ...) returns the file count; each path is then
// queried by index (length first, excludes NUL, then a sized buffer). DragFinish
// always frees the shell-allocated drop buffer. Multi-file drop imports all into
// the active bank (bank-fill only — no assignment to any live instance).
// always frees the shell-allocated drop buffer. A .rsbank is a whole bank, not audio,
// so it routes to the import verb (one new bank each); everything else keeps the
// existing ingest route — multi-file drop imports all into the active bank (bank-fill
// only, no assignment to any live instance).
void handleDropFiles(HDROP hDrop) {
std::vector<std::string> paths;
std::vector<std::string> packages;
const UINT count = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0);
paths.reserve(count);
for (UINT i = 0; i < count; ++i) {
@@ -54,9 +67,14 @@ void handleDropFiles(HDROP hDrop) {
std::vector<char> buf(static_cast<std::size_t>(len) + 1, '\0');
DragQueryFile(hDrop, i, buf.data(), static_cast<UINT>(buf.size()));
std::string p(buf.data());
if (!p.empty()) paths.push_back(std::move(p));
if (p.empty()) continue;
if (isPackagePath(p)) packages.push_back(std::move(p));
else paths.push_back(std::move(p));
}
DragFinish(hDrop);
if (g_panel.session)
for (const std::string& pkg : packages)
doImportBankPackageFile(*g_panel.session, pkg);
if (!paths.empty()) ingestDroppedFiles(paths);
}