26e2bf2fc6
S17: drag_out InstrumentDrop gesture + instrument_drop blob reusing the instrument's own serializer; bank_panel FX hover-track + add-VST/vst_chunk inject. S13 relay deferred (read-only bridge) — editor shows drop affordance.
141 lines
8.9 KiB
C++
141 lines
8.9 KiB
C++
#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.)
|
|
//
|
|
// S17 (drop-and-load) adds two inputs that refine the OUTSIDE-the-panel decision without
|
|
// touching the INSIDE decision (the internal bank-to-bank drag stays byte-identical):
|
|
// * singleCapture — the payload holds EXACTLY ONE sample id. Only a single-capture drag
|
|
// arms the InstrumentDrop gesture (per the S17 open-question lean: a multi-capture drag
|
|
// over an FX button is NOT an instrument drop — it falls through to OsDrag, the natural
|
|
// multi-file drag-out to Explorer/another DAW). REJECT, not load-first: the whole gesture
|
|
// is "make ONE capture a playable instrument", so a multi payload is out of contract here.
|
|
// * overReaperUi — a SHELL-SUPPLIED predicate: true when the pointer, though outside the
|
|
// panel client rect, is still over REAPER's OWN window/UI (the shell owns the REAPER
|
|
// hit query, e.g. GetThingFromPoint; the pure layer owns only the set/boundary algebra).
|
|
// Both default false, so an M11-era caller that fills only {dragging, hasArmedSamples} gets
|
|
// EXACTLY the M11 behavior: outside the client rect with overReaperUi=false -> OsDrag.
|
|
struct DragState {
|
|
bool dragging = false; // threshold crossed; a drag is in progress
|
|
bool hasArmedSamples = false; // the drag payload holds >= 1 sample id
|
|
bool singleCapture = false; // S17: payload holds EXACTLY one sample (arms InstrumentDrop)
|
|
bool overReaperUi = false; // S17: pointer is over REAPER's own UI (shell-supplied)
|
|
};
|
|
|
|
// 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
|
|
InstrumentDrop, // S17: single-capture drag left the panel but is over REAPER's UI —
|
|
// the shell hover-tracks the TCP FX button and, on release, adds a
|
|
// ReaSampler 9000 instance preloaded with the dragged capture.
|
|
OsDrag, // dragging with samples, pointer left REAPER entirely — 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 OUTSIDE the client rect, SINGLE capture, over REAPER's UI: InstrumentDrop —
|
|
// the drag is heading for a track's FX button (S17); the shell hover-tracks + highlights.
|
|
// * Dragging OUTSIDE the client rect otherwise (multi-capture, OR the pointer has left
|
|
// REAPER entirely): OsDrag — the samples are leaving to the OS; the shell initiates the
|
|
// native OS drag with the resolved paths.
|
|
// The INSIDE decision is untouched (M11 internal drag is byte-identical). The M11 boundary
|
|
// (left the client rect -> OsDrag) is REFINED, not replaced: leaving the rect now asks
|
|
// "single-capture and over REAPER's UI -> InstrumentDrop, else -> OsDrag" — so the M11
|
|
// OS-drag-out (multi payload, or pointer off REAPER) keeps its exact behavior. Position-only
|
|
// + state-only (no hidden state), so re-entry back inside returns Internal.
|
|
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
|