Hand a card dragged onto a plugin's own editor to the OS as a file drop; withdraw the OLE drag when the pointer returns to REAPER
A code-ownership proof (window class procedure's module, via the pure window_origin) plus REAPER's FX-surface token routes a plugin editor to OLE; FX chrome keeps the instrument drop. The OLE loop re-asks the law each turn, so passing a plugin editor stays reversible.
This commit is contained in:
@@ -57,8 +57,8 @@ is owned by other directories and only skinned here.
|
||||
|
||||
- `shell/actions` (`action_registry` / `design_view_actions` / `bank_actions` / `prune_action`) — the bindable action families, all routed via the `command_id`/`gaccel`/`hookcommand` contract. `action_registry` owns the shared registration plumbing (interned channel-qualified id strings; register and mirror-unregister present the identical pointer) **and the Q-W6 registration TABLE**: `main.cpp`'s own family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is one `ActionTableRow` array — suffix, phrase, flat function-pointer handler — that registration, hookcommand dispatch, and the unload mirror-unregister all iterate, so adding an action touches the table only (OCP). Bank mutations flow through the promptless `shell/bank_ops` verbs (`bankOp*` + `persistBankOp`, taking `ReaSamplerSession&`), which the panel menus and `bank_actions` consume as thin UX skins; `src/shell/bank_ops/CLAUDE.md` owns the one-op-one-Ctrl-Z rule those verbs enforce. The prune action (`prune_action`, `BANK_PRUNE_FOLDER`) is **the ONLY file-deletion action in the system**; it opens no undo point (file deletion is not REAPER-undoable). `BANK_PRUNE_FOLDER` halts on `blockedByTracking` and prints each blocker that fired, with recovery instructions.
|
||||
- `package_export_action` — the "export bank as package" skin: survey and report first, confirm what is absent (and, separately, a destination being replaced), pick a destination, write. Every prompt in the flow lives here so `shell/package/export_bank` stays promptless. Read-only against the project — it holds the session by `const&`, so no ext-state write, generation bump or undo point is reachable. Registration rides `main.cpp`'s action table (`EXPORT_BANK_PACKAGE`); the panel's tab menu is the second skin over the same body.
|
||||
- `drag_out_win` — the host/OS boundary of drag-out: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`. Also owns `pointerOverHostWindow`, the window-ownership test behind `ui::DropContext::pointerOffHost` (`core/ui/CLAUDE.md` owns the hand-off rule it feeds) — process identity on Windows (`WindowFromPoint` + `GetWindowThreadProcessId`), non-null `WindowFromPoint` under SWELL, whose window list is already this-process-only. Its shipped exception: a plugin bridged into `reaper_host*.exe` reports off-host though the pointer never left REAPER.
|
||||
- `instrument_drop_win` — instrument-drop shell: `probeDropTarget` resolves a screen point to a track + a `ReaperSurface` (via the pure `wire::classifyReaperSurface`, whose token rules `core/wire/CLAUDE.md` owns) — it answers only what REAPER named, never whether the pointer is still in REAPER — and the drop half adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.**
|
||||
- `drag_out_win` — the host/OS boundary of drag-out: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`. Owns the two window tests behind `ui::DropContext`'s hand-off proofs (`core/ui/CLAUDE.md` owns the rule they feed). `pointerOverHostWindow` is process identity on Windows (`WindowFromPoint` + `GetWindowThreadProcessId`), non-null `WindowFromPoint` under SWELL, whose window list is already this-process-only; its shipped exception: a plugin bridged into `reaper_host*.exe` reports off-host though the pointer never left REAPER. `pointerOverPluginEditor` walks from the hit window to its root reading each window's owning process and the module of its CLASS procedure (`GetClassLongPtr(GCLP_WNDPROC)` → `GetModuleHandleEx(FROM_ADDRESS)`), and lets `ui::window_origin` decide; its failure modes are listed at the declaration, and it is always false under SWELL. The OLE drop source re-asks the gesture law on every `QueryContinueDrag` (a function pointer the panel supplies) and withdraws the drag per `ui::decideOsDragTurn`, reporting it as `OsDragEnd::Withdrawn*` so the panel resumes its own gesture. A drop into a plugin writes nothing to the bank and inserts no timeline item — the plugin receives a copy-only path list, exactly as from Explorer.
|
||||
- `instrument_drop_win` — instrument-drop shell: `probeDropTarget` resolves a screen point to a track + a `ReaperSurface` (via the pure `wire::classifyReaperSurface`, whose token rules `core/wire/CLAUDE.md` owns) — it answers only what REAPER named, never whether the pointer is still in REAPER or over a plugin's own editor (so an FX surface reaches the instrument drop only as its chrome; a plugin's editor inside it hands off to the OS) — and the drop half adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.**
|
||||
- `insert_fx_action` — the "Insert as FX" verb: a second ENTRY POINT to `instrument_drop_win`'s drop body (the bank panel's Placement button and a bindable action), loading the panel's focused capture onto the first selected track. Owns no insert logic of its own — it resolves the selection, folds `core/ui/insert_fx_enable`, and calls `performInstrumentDrop` (which keeps the one undo block). Refuses visibly on every failing precondition; the panel button additionally paints dead for the two panel-local ones.
|
||||
- `arrange_drop_win` — the drag-out gesture's arrange outcome: `arrangeTimeAtScreenX` (pointer column → time via `GetSet_ArrangeView2`'s one-pixel-span reading — inferred, not SDK-documented) and `performArrangeDrop` (snap the drop time, then one `InsertMedia` per capture on the pointer's track — assumed, not confirmed, to land end-to-end via REAPER's own cursor advance — in ONE undo block, counting only InsertMedia's reported successes, with the caller's track selection and edit cursor restored). The one timeline-placing shell here, per the invariant above; it never captures and never writes the bank.
|
||||
- `package_import_action` — the bindable / bank-menu / file-drop skin over `shell/package`'s `importBankPackage`. Owns the **ledger gate**, which runs BEFORE the file picker (a refusal must not cost the user a file choice) and is keyed on the session's `LedgerStatus` alone — never on `PruneReport::blockedByTracking`, whose undecodable-`rsusage_*` arm governs deletion-time protection and would refuse an import that only writes birth records. Builds and shows every message the import produces, but the ledger-refusal body itself is `core/package::ledgerRefusalMessage` — a pure fold this TU only supplies the channel-correct namespace to — so the wording is assertable without a DAW. `doImportBankPackage`/`doImportBankPackageFile` return the minted bank id on a landed import (empty otherwise) so a caller can focus it; the verb itself is promptless.
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
#include <shlobj.h> // DROPFILES, CF_HDROP
|
||||
#include <cstring>
|
||||
|
||||
#include "core/ui/drag_out.h" // decideOsDragTurn — the OS loop's per-turn verdict
|
||||
#include "core/ui/window_origin.h" // classifyWindowCode / isPluginEditorChain
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
@@ -86,10 +89,16 @@ bool ensureOleForThisThread() {
|
||||
return state > 0;
|
||||
}
|
||||
|
||||
// Minimal IDropSource: continue until the (left) button releases or Escape cancels; always
|
||||
// request the copy cursor. This is the standard textbook drop source — no custom feedback.
|
||||
// IDropSource: the textbook button/Escape source, plus the law re-asked every turn so the drag can
|
||||
// be withdrawn back to the panel (ui::decideOsDragTurn). Always requests the default cursors.
|
||||
// [verify — DAW] QueryContinueDrag's cadence: MSDN words it as called on a keyboard or
|
||||
// mouse-BUTTON state change, so on plain moves it may not run; if so a mid-drag withdrawal waits
|
||||
// for the next state change, but the release — a button change — is still decided here.
|
||||
class DropSource final : public IDropSource {
|
||||
public:
|
||||
explicit DropSource(bool (*lawHandsOff)()) : lawHandsOff_(lawHandsOff) {}
|
||||
ui::OsDragTurn withdrawal() const { return withdrawal_; }
|
||||
|
||||
// IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {
|
||||
if (riid == IID_IUnknown || riid == IID_IDropSource) {
|
||||
@@ -108,15 +117,33 @@ public:
|
||||
}
|
||||
// IDropSource
|
||||
HRESULT STDMETHODCALLTYPE QueryContinueDrag(BOOL escapePressed, DWORD keyState) override {
|
||||
if (escapePressed) return DRAGDROP_S_CANCEL;
|
||||
if (!(keyState & MK_LBUTTON)) return DRAGDROP_S_DROP; // released -> drop
|
||||
return S_OK; // keep dragging
|
||||
const bool escape = escapePressed != FALSE;
|
||||
// Escape ignores the law, so it is not probed then; no callback is the textbook source.
|
||||
const bool lawHandsOff = escape || !lawHandsOff_ || lawHandsOff_();
|
||||
const ui::OsDragTurn turn =
|
||||
ui::decideOsDragTurn(escape, (keyState & MK_LBUTTON) != 0, lawHandsOff);
|
||||
switch (turn) {
|
||||
case ui::OsDragTurn::Continue:
|
||||
return S_OK;
|
||||
case ui::OsDragTurn::Drop:
|
||||
return DRAGDROP_S_DROP;
|
||||
case ui::OsDragTurn::Cancel:
|
||||
return DRAGDROP_S_CANCEL;
|
||||
case ui::OsDragTurn::Withdraw:
|
||||
case ui::OsDragTurn::WithdrawAtRelease:
|
||||
withdrawal_ = turn; // the target sees a cancel (DragLeave), never a drop
|
||||
return DRAGDROP_S_CANCEL;
|
||||
}
|
||||
return DRAGDROP_S_CANCEL;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GiveFeedback(DWORD /*effect*/) override {
|
||||
return DRAGDROP_S_USEDEFAULTCURSORS; // let OLE draw the standard copy cursor
|
||||
}
|
||||
private:
|
||||
volatile LONG refs_ = 1;
|
||||
bool (*lawHandsOff_)() = nullptr;
|
||||
// Continue doubles as "not withdrawn": it is never the turn that ends the loop.
|
||||
ui::OsDragTurn withdrawal_ = ui::OsDragTurn::Continue;
|
||||
};
|
||||
|
||||
// Minimal IDataObject exposing exactly one format (CF_HDROP / TYMED_HGLOBAL). The HDROP is
|
||||
@@ -214,25 +241,34 @@ private:
|
||||
|
||||
} // namespace
|
||||
|
||||
bool initiateDragOut(HWND__* /*panelHwnd*/, const std::vector<std::string>& absolutePaths) {
|
||||
if (absolutePaths.empty()) return false;
|
||||
if (!ensureOleForThisThread()) return false;
|
||||
OsDragEnd initiateDragOut(HWND__* /*panelHwnd*/, const std::vector<std::string>& absolutePaths,
|
||||
bool (*lawHandsOff)()) {
|
||||
if (absolutePaths.empty()) return OsDragEnd::Concluded;
|
||||
if (!ensureOleForThisThread()) return OsDragEnd::Concluded;
|
||||
|
||||
HGLOBAL hdrop = buildHDrop(absolutePaths);
|
||||
if (!hdrop) return false;
|
||||
if (!hdrop) return OsDragEnd::Concluded;
|
||||
|
||||
auto* data = new HDropDataObject(hdrop); // takes ownership of hdrop
|
||||
auto* source = new DropSource();
|
||||
auto* source = new DropSource(lawHandsOff);
|
||||
|
||||
DWORD effect = 0;
|
||||
// COPY-ONLY: the allowed-effects mask is DROPEFFECT_COPY alone. MOVE is never
|
||||
// offered, so no drop target can relocate (delete) the bank file.
|
||||
const HRESULT hr = DoDragDrop(data, source, DROPEFFECT_COPY, &effect);
|
||||
DoDragDrop(data, source, DROPEFFECT_COPY, &effect);
|
||||
|
||||
const ui::OsDragTurn withdrawal = source->withdrawal();
|
||||
source->Release();
|
||||
data->Release(); // frees the source HGLOBAL via HDropDataObject's dtor
|
||||
|
||||
return hr == DRAGDROP_S_DROP && effect == DROPEFFECT_COPY;
|
||||
switch (withdrawal) {
|
||||
case ui::OsDragTurn::Withdraw: return OsDragEnd::Withdrawn;
|
||||
case ui::OsDragTurn::WithdrawAtRelease: return OsDragEnd::WithdrawnAtRelease;
|
||||
case ui::OsDragTurn::Continue:
|
||||
case ui::OsDragTurn::Drop:
|
||||
case ui::OsDragTurn::Cancel: break;
|
||||
}
|
||||
return OsDragEnd::Concluded;
|
||||
}
|
||||
|
||||
bool canInitiateDragOut(const std::vector<std::string>& absolutePaths) {
|
||||
@@ -265,6 +301,85 @@ bool pointerOverHostWindow(int screenX, int screenY) {
|
||||
return pid == GetCurrentProcessId();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::string toUtf8(const std::wstring& w) {
|
||||
if (w.empty()) return {};
|
||||
const int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), nullptr,
|
||||
0, nullptr, nullptr);
|
||||
if (n <= 0) return {};
|
||||
std::string s(static_cast<std::size_t>(n), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), &s[0], n, nullptr,
|
||||
nullptr);
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string moduleFileName(HMODULE mod) {
|
||||
std::wstring buf(MAX_PATH, L'\0');
|
||||
for (;;) {
|
||||
const DWORD n = GetModuleFileNameW(mod, &buf[0], static_cast<DWORD>(buf.size()));
|
||||
if (n == 0) return {};
|
||||
if (n < buf.size()) { buf.resize(n); return toUtf8(buf); }
|
||||
if (buf.size() >= 32768) return {}; // truncated at the long-path limit: no evidence
|
||||
buf.resize(buf.size() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Empty on failure, which window_origin reads as "cannot tell OS code from plugin code".
|
||||
const std::string& systemRoot() {
|
||||
static const std::string root = [] {
|
||||
wchar_t dir[MAX_PATH] = {0};
|
||||
const UINT n = GetSystemWindowsDirectoryW(dir, MAX_PATH);
|
||||
return (n > 0 && n < MAX_PATH) ? toUtf8(std::wstring(dir, n)) : std::string{};
|
||||
}();
|
||||
return root;
|
||||
}
|
||||
|
||||
ui::WindowFacts windowFacts(HWND hwnd) {
|
||||
ui::WindowFacts f;
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(hwnd, &pid);
|
||||
if (pid != GetCurrentProcessId()) {
|
||||
f.otherProcess = true;
|
||||
return f;
|
||||
}
|
||||
// The charset-matched read returns the procedure's address; a mismatched one can return a
|
||||
// handle that lies in no module, which is left as unresolved rather than guessed at.
|
||||
const ULONG_PTR proc = IsWindowUnicode(hwnd) ? GetClassLongPtrW(hwnd, GCLP_WNDPROC)
|
||||
: GetClassLongPtrA(hwnd, GCLP_WNDPROC);
|
||||
HMODULE mod = nullptr;
|
||||
if (!proc || !GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(proc), &mod) ||
|
||||
!mod) {
|
||||
return f;
|
||||
}
|
||||
f.procResolved = true;
|
||||
f.hostModule = mod == GetModuleHandleW(nullptr);
|
||||
f.modulePath = moduleFileName(mod);
|
||||
return f;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool pointerOverPluginEditor(int screenX, int screenY) {
|
||||
// The CLASS procedure's module rather than the window's hInstance: a bare-SDK plugin may
|
||||
// create its editor with the hInstance of the parent REAPER hands it (ReaSampler 9000's own
|
||||
// editor does), so hInstance would read that plugin as REAPER.
|
||||
const POINT pt{screenX, screenY};
|
||||
const HWND hit = WindowFromPoint(pt);
|
||||
if (!hit) return false;
|
||||
const HWND root = GetAncestor(hit, GA_ROOT);
|
||||
|
||||
constexpr std::size_t kMaxDepth = 64; // a guard against a malformed tree, not a real bound
|
||||
std::vector<ui::WindowCode> chain;
|
||||
for (HWND h = hit; h && chain.size() < kMaxDepth; h = GetAncestor(h, GA_PARENT)) {
|
||||
chain.push_back(ui::classifyWindowCode(windowFacts(h), systemRoot()));
|
||||
if (h == root) break;
|
||||
}
|
||||
return ui::isPluginEditorChain(chain);
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
#else // ---- macOS / Linux (SWELL) -----------------------------------------------------
|
||||
@@ -278,8 +393,9 @@ namespace reasampler {
|
||||
// SWELL_InitiateDragDropOfFileList initiates a copy-style file drag from the given
|
||||
// window. Unlike OLE it exposes no per-source effect mask, so the copy-only
|
||||
// guarantee here rests on SWELL's copy semantics rather than an explicit mask.
|
||||
bool initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths) {
|
||||
if (absolutePaths.empty() || !panelHwnd) return false;
|
||||
OsDragEnd initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths,
|
||||
bool (* /*lawHandsOff*/)()) {
|
||||
if (absolutePaths.empty() || !panelHwnd) return OsDragEnd::Concluded;
|
||||
|
||||
std::vector<const char*> ptrs;
|
||||
ptrs.reserve(absolutePaths.size());
|
||||
@@ -288,7 +404,8 @@ bool initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolute
|
||||
// srcrect null: SWELL positions the drag image at the current event. No custom icon.
|
||||
SWELL_InitiateDragDropOfFileList(reinterpret_cast<HWND>(panelHwnd), nullptr,
|
||||
ptrs.data(), static_cast<int>(ptrs.size()), nullptr);
|
||||
return true; // fire-and-forget; SWELL owns the drag from here (no accept/cancel return)
|
||||
// Fire-and-forget; SWELL owns the drag from here, with no per-turn hook to re-ask the law.
|
||||
return OsDragEnd::Concluded;
|
||||
}
|
||||
|
||||
// SWELL exposes no readiness probe ahead of SWELL_InitiateDragDropOfFileList (which itself
|
||||
@@ -307,6 +424,11 @@ bool pointerOverHostWindow(int screenX, int screenY) {
|
||||
return WindowFromPoint(pt) != nullptr;
|
||||
}
|
||||
|
||||
// No plugin-editor hand-off off Windows: every FX window keeps its instrument outcome, as before.
|
||||
bool pointerOverPluginEditor(int /*screenX*/, int /*screenY*/) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
// drag_out_win — the host/OS boundary of native OS drag-out: whether the pointer has
|
||||
// crossed it, whether the OS is ready to take the drag, and the hand-off itself, which
|
||||
// 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.*.
|
||||
@@ -21,11 +22,22 @@ struct HWND__; // avoid dragging windows.h into every includer; the shell casts
|
||||
|
||||
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 until drop/cancel. Returns true iff the drop was accepted
|
||||
// (DROPEFFECT_COPY); the return is advisory — a failed drag surfaces no error.
|
||||
bool initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths);
|
||||
// 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
|
||||
@@ -44,4 +56,18 @@ bool canInitiateDragOut(const std::vector<std::string>& absolutePaths);
|
||||
// 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.
|
||||
// [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.
|
||||
// SWELL: always false (no plugin-editor hand-off off Windows). Side-effect-free.
|
||||
bool pointerOverPluginEditor(int screenX, int screenY);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -199,10 +199,11 @@ LiveDrop resolveLiveDrop(int x, int y) {
|
||||
const DropProbe probe = probeDropTarget(sp.x, sp.y);
|
||||
ctx.surface = probe.surface;
|
||||
ctx.haveTrack = probe.track != nullptr;
|
||||
// Two probes because they answer two independent questions: what REAPER named, and
|
||||
// whether we are still inside REAPER at all. The law's hand-off gate reads both — see
|
||||
// ui::DropContext::pointerOffHost.
|
||||
// Separate probes because they answer independent questions: what REAPER named, whether
|
||||
// we are still inside REAPER at all, and whether plugin code owns the window. The law's
|
||||
// hand-off gates pair them — see ui::DropContext::pointerOffHost.
|
||||
ctx.pointerOffHost = !pointerOverHostWindow(sp.x, sp.y);
|
||||
ctx.pointerOverPluginEditor = pointerOverPluginEditor(sp.x, sp.y);
|
||||
out.track = probe.track;
|
||||
out.screenX = sp.x;
|
||||
}
|
||||
@@ -219,16 +220,46 @@ LiveDrop resolveLiveDrop(int x, int y) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// The law's one irreversible transition: DoDragDrop takes mouse capture and runs its own modal
|
||||
// loop, so the internal drag must be fully wound down first, and only once the payload is known
|
||||
// to be hand-off-able. This runs on every qualifying move — do not memoize a failed attempt.
|
||||
//
|
||||
// Residual (accepted): once the pointer has genuinely left every REAPER window, dragging back IN
|
||||
// mid-modal-loop delivers a CF_HDROP to REAPER's own file-import drop target rather than to our
|
||||
// gesture law. NOT confirmed by experiment — inferred from REAPER's handling of external file
|
||||
// drops, and the inferred outcome (an item at the drop point) coincides with what our own
|
||||
// arrange path would have done. This is now the ONLY way to see that import ghost mid-drag;
|
||||
// reaching it from a surface still inside REAPER was the defect pointerOffHost closed.
|
||||
// Re-asked by the OS drag loop on every turn. The payload fields the law reads survive
|
||||
// resetDragState, so it still sees the drag the hand-off wound down. With the panel gone
|
||||
// mid-loop there is no gesture left to give back, so the OS keeps the drag.
|
||||
bool lawHandsOffAtCursor() {
|
||||
if (!g_panel.hwnd) return true;
|
||||
POINT p{};
|
||||
GetCursorPos(&p);
|
||||
ScreenToClient(g_panel.hwnd, &p);
|
||||
return resolveLiveDrop(p.x, p.y).cls == DropClass::OsHandoff;
|
||||
}
|
||||
|
||||
// The OS loop gave the gesture back (ui::OsDragTurn::Withdraw*): restore exactly the fields
|
||||
// resetDragState cleared that the drag still needs, then either keep dragging under our own
|
||||
// capture or — the release was consumed by the OS loop, so no WM_LBUTTONUP will follow — commit
|
||||
// at the release point now. No move is replayed here: the next WM_MOUSEMOVE re-resolves the cue,
|
||||
// and replaying one could re-enter handOffToOs from inside itself.
|
||||
void resumeInternalDrag(const std::string& primaryId, bool released) {
|
||||
if (!g_panel.hwnd) return;
|
||||
g_panel.dragArmed = true;
|
||||
g_panel.dragging = true;
|
||||
g_panel.dragPrimaryId = primaryId;
|
||||
|
||||
POINT p{};
|
||||
GetCursorPos(&p);
|
||||
ScreenToClient(g_panel.hwnd, &p);
|
||||
if (released) {
|
||||
onLBtnUp(p.x, p.y);
|
||||
return;
|
||||
}
|
||||
// [verify — DAW] that SetCapture re-takes the mouse mid-press after DoDragDrop released it.
|
||||
SetCapture(g_panel.hwnd);
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
// The hand-off: DoDragDrop takes mouse capture and runs its own modal loop, so the internal drag
|
||||
// must be fully wound down first, and only once the payload is known to be hand-off-able. This
|
||||
// runs on every qualifying move — do not memoize a failed attempt. The OS loop may hand the
|
||||
// gesture back (ui::OsDragTurn owns why); a side effect worth keeping is that, on Windows, a
|
||||
// return trip can no longer drop a CF_HDROP on REAPER's file import or on this panel's own
|
||||
// WM_DROPFILES ingest — the release there is withdrawn, not dropped.
|
||||
void handOffToOs() {
|
||||
// Resolve BEFORE tearing anything down (the resolver reads the live drag payload), then let
|
||||
// the pure rule couple the two side effects: an unresolvable payload leaves the internal
|
||||
@@ -242,11 +273,22 @@ void handOffToOs() {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string primaryId = g_panel.dragPrimaryId; // resetDragState clears it
|
||||
if (GetCapture() == g_panel.hwnd) ReleaseCapture();
|
||||
resetDragState();
|
||||
invalidatePanel();
|
||||
|
||||
initiateDragOut(g_panel.hwnd, paths); // COPY-ONLY; blocking on Windows
|
||||
// COPY-ONLY; blocking on Windows.
|
||||
switch (initiateDragOut(g_panel.hwnd, paths, &lawHandsOffAtCursor)) {
|
||||
case OsDragEnd::Withdrawn:
|
||||
resumeInternalDrag(primaryId, /*released=*/false);
|
||||
return;
|
||||
case OsDragEnd::WithdrawnAtRelease:
|
||||
resumeInternalDrag(primaryId, /*released=*/true);
|
||||
return;
|
||||
case OsDragEnd::Concluded:
|
||||
break;
|
||||
}
|
||||
|
||||
// The drag state was reset above, so the WM_LBUTTONUP that ends the modal loop takes
|
||||
// onLBtnUp's not-dragging branch and never restores the arrow — do it here instead.
|
||||
|
||||
Reference in New Issue
Block a user