Files
reasampler/src/shell/actions/drag_out_win.h
T
daniel faa513cafb Fix SWELL drag guard, channel-fork the VST editor window class, dedupe own-module match
Panel drag no longer misfires on SWELL's always-zero wParam; stable/beta VST editors register distinct window classes against their own module instead of colliding on REAPER's; own-module basename matching moves to a tested pure helper.
2026-09-25 14:11:22 -04:00

92 lines
6.0 KiB
C++

#pragma once
// drag_out_win — the host/OS boundary of native OS drag-out: whether the pointer has
// crossed it (left the host process, or reached a plugin's own editor), whether the OS is
// ready to take the drag, and the hand-off itself, which
// gives a resolved existing-file path list to the OS's drag-drop machinery so the user can
// drop bank samples into Explorer / another app / another DAW. The pure gesture-boundary
// decision + path-list assembly live in drag_out.*.
//
// COPY-ONLY is STRUCTURAL on Windows: DoDragDrop's effect mask is DROPEFFECT_COPY
// alone — MOVE is never offered, so no target can pull a file out of the bank folder
// (only prune deletes bank bytes). macOS/Linux route through SWELL's file-list drag,
// which exposes no per-source effect mask, so there the copy-only guarantee rests on
// SWELL's copy semantics rather than an explicit mask.
//
// NON-DESTRUCTIVE: initiating a drag reads nothing but the path list; a cancelled or
// failed drag mutates no sample/index/selection.
#include <string>
#include <vector>
struct HWND__; // avoid dragging windows.h into every includer; the shell casts as needed.
namespace reasampler {
// How the OS drag ended, as far as the caller must act on it. Whether a Concluded drag was
// accepted is not reported — nothing acts on it.
enum class OsDragEnd {
Concluded, // dropped, cancelled, or failed to start: the gesture is over
Withdrawn, // ui::OsDragTurn::Withdraw — the button is still down
WithdrawnAtRelease, // ui::OsDragTurn::WithdrawAtRelease — the button came up over REAPER
};
// `absolutePaths` must already be resolved/existing/de-duped (drag_out::assemblePathList
// output). No-op when empty. BLOCKING on Windows: OLE DoDragDrop runs its own modal
// message loop, re-asking `lawHandsOff` (the gesture law at the current pointer) on every
// IDropSource::QueryContinueDrag and withdrawing per ui::decideOsDragTurn. macOS/Linux: SWELL's
// file drag is fire-and-forget with no such hook, so it always reports Concluded — the hand-off
// stays irreversible there.
OsDragEnd initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths,
bool (*lawHandsOff)());
// Side-effect-free readiness probe for initiateDragOut. Call BEFORE tearing down internal drag
// state — an OS that isn't ready (OLE unavailable, HDROP build failure) must not consume the
// gesture like an empty payload would. Windows re-runs the OLE-init + HDROP checks and frees the
// probe HGLOBAL immediately; SWELL exposes no probe, so macOS/Linux reduces to the non-empty check.
bool canInitiateDragOut(const std::vector<std::string>& absolutePaths);
// True iff a window belonging to the host (REAPER) process sits under the screen point — the
// POSITIVE proof ui::DropContext::pointerOffHost is the negation of (that member owns the
// evidence rule). Nearly every REAPER surface qualifies whether or not GetThingFromPoint can
// name it, because the extension runs inside that process.
// The exception, and it ships: a plugin set to "Run as separate/dedicated process" draws its
// UI out of reaper_host32/64.exe, and WindowFromPoint descends to the deepest child — so this
// reports false over a bridged plugin's editor although the pointer never left REAPER. The
// law's hand-off gate absorbs that by also requiring REAPER's own hit-test to name nothing.
// Side-effect-free; safe on every mouse-move.
bool pointerOverHostWindow(int screenX, int screenY);
// True iff plugin code implements the window under the screen point or one of its ancestors —
// the POSITIVE proof behind ui::DropContext::pointerOverPluginEditor. Windows reads, per window
// from the hit window up to its root, the owning process and the module holding the window
// CLASS's procedure, and lets ui::window_origin decide. The failure modes, all toward REAPER's
// own outcome (the instrument drop) rather than toward OLE:
// - a plugin that draws straight into the parent REAPER hands it, creating no window of its
// own, leaves nothing of its code in the chain;
// - a plugin binary installed beneath the Windows directory reads as an OS module;
// - a class procedure that resolves to no module (a charset-mismatch handle) is no evidence;
// - an all-#32770 dialog-based editor (the generic Windows dialog class) resolves to no
// recognizable plugin module — falls to REAPER's outcome;
// - a .NET/WPF editor's class procedure is a CLR-thunked address with no resolvable module —
// also falls to REAPER's outcome; `[verify]`;
// - our own ReaSampler 9000 module (this channel's reasampler_9000[_beta].vst3, matched by
// basename) also falls to REAPER's outcome, deliberately — see ui::WindowFacts::ownModule;
// - ReaSampler 9000 itself run bridged/in a separate process reads as Plugin, same as any other
// bridged plugin — banner (the OS hand-off fires over our own editor), harmless;
// - a renamed ReaSampler 9000 binary (the basename no longer matches ownVstFileName()) misses
// the exemption and reads as Plugin — banner, harmless.
// The opposite direction is unguarded: a third-party hook that replaces REAPER's own chrome
// class procedure via SetClassLongPtr(GCLP_WNDPROC) would make REAPER's own window read as
// Plugin — toward OLE instead of away from it.
// [verify — DAW] that REAPER's GetThingFromPoint names an fx_* token over the plugin's own child
// window — the law requires it, so an unnamed editor would refuse rather than hand off.
// [verify — DAW] that OLE never walks up to an ancestor drop target (e.g. REAPER's own file
// import) when the plugin editor's window registers no IDropTarget of its own.
// [verify — DAW] that GetThingFromPoint cannot name a REAPER region (e.g. an fx_* token) when a
// foreign window overlaps it at the same screen point — this fn trusts WindowFromPoint's hit
// over whatever GetThingFromPoint separately reports.
// SWELL: always false (no plugin-editor hand-off off Windows). Side-effect-free.
bool pointerOverPluginEditor(int screenX, int screenY);
} // namespace reasampler