feat(m11): native OS drag-out of bank samples (copy-only)

Pure drag_out (gesture boundary + path-list assembly) + Windows OLE
CF_HDROP / SWELL file-list shell; bank_panel hands off when a dragged
selection leaves the client area. COPY-ONLY mask; internal drag intact.
This commit is contained in:
2026-07-26 20:02:46 -04:00
parent 73f81ffbb7
commit 34df2563e4
7 changed files with 738 additions and 1 deletions
+61
View File
@@ -48,6 +48,8 @@
#include "action_buttons.h" // pure button-strip layout + label format (M11)
#include "actions.h" // persistBankOp — shared undo-block wrapper (R-B panel path)
#include "drag_out.h" // pure gesture-boundary decision + path-list assembly (M11)
#include "drag_out_win.h" // OLE / SWELL drag-out initiation seam (M11)
#include "app_version.h" // channelCommandId — compose the named-command lookup string (M11)
#include "bank_book.h"
#include "bank_grid.h"
@@ -1616,6 +1618,34 @@ std::vector<std::string> focusedSelectionIds() {
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 BankIndex* 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
@@ -2076,6 +2106,37 @@ void onMouseMove(int x, int y) {
}
}
if (g_panel.dragging) {
// M11 gesture boundary (invariant #4): while a drag with samples is under way, the
// moment the pointer LEAVES the panel client area the gesture becomes OS-bound —
// hand the payload to the native OS drag. Inside the client area it stays the
// existing internal bank-to-bank drag, byte-identical. The boundary decision is the
// pure drag_out::decideGesture (drag state + pointer + client rect).
RECT cr{};
GetClientRect(g_panel.hwnd, &cr);
const PanelClientRect client{cr.left, cr.top, cr.right - cr.left, cr.bottom - cr.top};
const DragState st{/*dragging=*/true, /*hasArmedSamples=*/!g_panel.dragSampleIds.empty()};
if (decideGesture(x, y, client, st) == DragGesture::OsDrag) {
// Resolve the payload to existing on-disk paths BEFORE tearing down internal
// drag state (the resolver reads dragSourceBankId / dragSampleIds).
const std::vector<std::string> paths = resolveDragPathsForOs();
// Reset internal drag state and release capture NOW: DoDragDrop runs its own
// modal loop and takes over mouse capture, so the internal drag must be fully
// wound down first (no stale dragging/dropKind, no lingering SetCapture). A
// cancelled/empty OS drag therefore leaves the panel in a clean, no-op state
// (invariant #2 — nothing mutated).
if (GetCapture() == g_panel.hwnd) ReleaseCapture();
g_panel.dragArmed = false;
g_panel.dragging = false;
g_panel.dropKind = DropKind::None;
g_panel.dropBankId.clear();
invalidatePanel();
// Empty path list -> nothing draggable (all stale/missing); do not start a drag.
if (!paths.empty())
initiateDragOut(g_panel.hwnd, paths); // COPY-ONLY; blocking on Windows
return;
}
updateDropTarget(x, y);
invalidatePanel();
}