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:
2026-09-25 13:39:39 -04:00
parent 9ff34b0a9e
commit fe1aec5925
14 changed files with 720 additions and 91 deletions
+56 -14
View File
@@ -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.