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:
+118
@@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
// drag_out — the REAPER-free / OS-free decision logic behind the bank_panel's native OS
|
||||
// drag-out (Milestone 11, the final polish point). Two pure concerns live here so they are
|
||||
// unit-tested outside the DAW (CLAUDE.md §load-bearing split); the OLE / SWELL initiation
|
||||
// and the bank_panel gesture hook stay in the shell (drag_out_win.* + bank_panel.cpp).
|
||||
//
|
||||
// 1. GESTURE BOUNDARY (invariant #4 — do not regress the internal drag). The panel
|
||||
// already runs an INTERNAL drag: press a selected cell, cross a threshold, drop onto
|
||||
// a pool/banks region or a tab to move/copy the samples between banks. That drag lives
|
||||
// entirely INSIDE the panel client rect. The OS drag is a DISTINCT gesture with a
|
||||
// distinct, discoverable boundary: while a drag is armed with samples in the payload,
|
||||
// the moment the pointer LEAVES the panel client area the gesture becomes OS-bound —
|
||||
// the payload is being dragged out to another window / Explorer / another DAW. Inside
|
||||
// the client area it stays internal; with no armed samples there is no drag at all.
|
||||
// This function is that decision, pure over (drag state + pointer + panel rect).
|
||||
//
|
||||
// 2. PATH-LIST ASSEMBLY. The OS drop carries absolute file paths (Windows CF_HDROP /
|
||||
// macOS file-list pasteboard). Turning the armed sample ids into that path list —
|
||||
// resolving each id to its already-on-disk bank file, de-duping, and applying an
|
||||
// explicit skip-missing-file policy — is pure string work over a resolver the shell
|
||||
// supplies (the shell owns the REAPER project-dir read + resolveBankFile; this module
|
||||
// owns the set algebra and the result contract). NO temp files: the bank files already
|
||||
// exist; the list points straight at them (COPY-ONLY is enforced at the OS layer — see
|
||||
// drag_out_win — never by relocating or copying bytes here).
|
||||
//
|
||||
// PURE MODULE: NO REAPER types, NO SWELL, NO OS/OLE, NO vendor/ includes. Standard library
|
||||
// only. Builds and unit-tests without REAPER. Mirror of action_buttons / mode_switch.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// --- Gesture boundary ---------------------------------------------------------
|
||||
|
||||
// The panel's client rectangle in its own client coordinates (top-left origin, the SWELL/
|
||||
// LICE convention). width/height are the extents; a point (px, py) is INSIDE when
|
||||
// x <= px < x + width and y <= py < y + height (half-open, matching the panel's other
|
||||
// hit-tests so the edge is claimed consistently).
|
||||
struct PanelClientRect {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
bool operator==(const PanelClientRect& o) const {
|
||||
return x == o.x && y == o.y && width == o.width && height == o.height;
|
||||
}
|
||||
};
|
||||
|
||||
// The live drag state the shell tracks, reduced to what the boundary decision needs:
|
||||
// whether a drag is currently active (threshold crossed) and whether the armed payload
|
||||
// carries at least one sample. (Pre-threshold "armed but not yet dragging" is NOT a drag
|
||||
// for this decision — the shell only asks once a drag is under way.)
|
||||
struct DragState {
|
||||
bool dragging = false; // threshold crossed; a drag is in progress
|
||||
bool hasArmedSamples = false; // the drag payload holds >= 1 sample id
|
||||
};
|
||||
|
||||
// What the shell should do with the drag given the current pointer position.
|
||||
enum class DragGesture {
|
||||
None, // no drag under way, or an empty payload — do nothing
|
||||
Internal, // dragging inside the panel — the existing bank-to-bank move/copy drag
|
||||
OsDrag, // dragging with samples, pointer left the client area — hand off to the OS
|
||||
};
|
||||
|
||||
// Decides the gesture for a drag at pointer (px, py) over `client`, given `state`.
|
||||
// * Not dragging (or no armed samples): None — the shell ignores the move.
|
||||
// * Dragging with samples, pointer INSIDE the client rect: Internal — unchanged
|
||||
// bank-to-bank behavior (invariant #4: the internal drag stays byte-identical).
|
||||
// * Dragging with samples, pointer OUTSIDE the client rect: OsDrag — the samples are
|
||||
// leaving the panel; the shell initiates the native OS drag with the resolved paths.
|
||||
// The boundary is the client rect edge: the internal drag never targets outside it, so
|
||||
// crossing it is an unambiguous, discoverable OS-drag trigger. Re-entry is the shell's
|
||||
// concern (the OS drag loop is modal once begun); this function reports OsDrag purely from
|
||||
// position, so a shell that has already handed off simply will not ask again.
|
||||
DragGesture decideGesture(int px, int py, const PanelClientRect& client,
|
||||
const DragState& state);
|
||||
|
||||
// --- Path-list assembly -------------------------------------------------------
|
||||
|
||||
// One armed sample reduced to what path assembly needs: the resolved ABSOLUTE file path
|
||||
// the shell computed for it (empty when the shell could not resolve it — e.g. no project
|
||||
// dir / empty relative path). The shell resolves each via the SAME machinery the panel
|
||||
// already uses for audition/insert (resolveBankFile over the current project dir), so the
|
||||
// drag points at the real bank file — no temp copy.
|
||||
struct ResolvedSample {
|
||||
std::string absolutePath; // resolved absolute path, or "" when unresolvable
|
||||
bool fileExists = false; // shell stat() result — drives the skip-missing policy
|
||||
};
|
||||
|
||||
// The outcome of assembling the drag's path list: the de-duped, existing-only absolute
|
||||
// paths to hand to the OS, plus explicit tallies so the shell can decide whether to
|
||||
// initiate at all (an empty `paths` means nothing draggable — do NOT start a drag).
|
||||
struct PathList {
|
||||
std::vector<std::string> paths; // de-duped, existing files, in first-seen order
|
||||
int skippedMissing = 0; // resolved but file did not exist (skip policy)
|
||||
int skippedUnresolved = 0; // shell could not resolve a path at all
|
||||
int skippedDuplicate = 0; // same absolute path seen more than once
|
||||
};
|
||||
|
||||
// Assembles the drag path list from the resolved samples (in selection order).
|
||||
// Policy (all explicit, all tested):
|
||||
// * SKIP-MISSING: a sample whose file does not exist on disk is skipped (counted in
|
||||
// skippedMissing) — a stale index entry must never put a dangling path on the OS
|
||||
// clipboard. This is the deliberate skip policy the brief asks be made explicit.
|
||||
// * SKIP-UNRESOLVED: an empty absolutePath (shell could not resolve) is skipped
|
||||
// (skippedUnresolved) — same reasoning, no empty entry reaches the OS.
|
||||
// * DEDUPE: the same absolute path appearing twice (two index entries, one file — the
|
||||
// cross-bank copy case) yields ONE CF_HDROP entry (skippedDuplicate counts the extras),
|
||||
// so the OS never sees a duplicate drop path. First occurrence wins; order preserved.
|
||||
// * EMPTY SELECTION: an empty input yields an empty PathList (all tallies zero) — the
|
||||
// shell reads paths.empty() and does not start a drag.
|
||||
// Comparison is exact-string (the shell normalizes slashes/case upstream if it wants
|
||||
// case-insensitive dedup on Windows — the pure layer does not guess a platform rule).
|
||||
PathList assemblePathList(const std::vector<ResolvedSample>& resolved);
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user