// panel_drag_os.cpp — the OS hand-off / resume seam split out of panel_drag.cpp: the live // re-ask the OS drag loop uses to decide whether to keep the drag, the hand-off itself, and // resuming the internal drag when the OS loop gives the gesture back. // // Compiled into the reaper_reasampler MODULE. No REAPER API functions are called directly here; // REAPER SDK types arrive via panel_state.h. #include #include #include "shell/panel/panel_state.h" #include "shell/actions/drag_out_win.h" // OLE / SWELL drag-out initiation seam namespace reasampler::panel { namespace { // The physical left button, honouring a swapped primary button (SM_SWAPBUTTON) — distinct from // dragging/dragArmed, which only say what OUR state machine last believed. Read fresh rather than // trusted, because DoDragDrop's own modal loop can consume the WM_LBUTTONUP that would otherwise // have told us the button came up. bool leftButtonPhysicallyDown() { const int vk = GetSystemMetrics(SM_SWAPBUTTON) ? VK_RBUTTON : VK_LBUTTON; return (GetAsyncKeyState(vk) & 0x8000) != 0; } // 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); // The button can have come up between DoDragDrop's own release and this re-capture (the // Withdrawn turn only promises the button was down AT the last OS-loop turn, not at this // instant) — without this check dragging stays armed with a stale payload, and the very next // plain click commits a reorder/replace nobody asked for. if (!leftButtonPhysicallyDown()) { onLBtnUp(p.x, p.y); return; } invalidatePanel(); } } // namespace // 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 // drag intact rather than winding it down for a hand-off that never runs — a half-torn-down // drag reads as "the drag did nothing, try again". canInitiateDragOut extends the same // coupling to the OS-readiness failures the pure decision cannot see. const std::vector paths = resolveDragPathsForOs(); if (!ui::decideOsHandoff(paths).handOffToOs || !canInitiateDragOut(paths)) { applyDropCue(DropCue::Refuse); invalidatePanel(); return; } const std::string primaryId = g_panel.dragPrimaryId; // resetDragState clears it if (GetCapture() == g_panel.hwnd) ReleaseCapture(); resetDragState(); invalidatePanel(); // 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. SetCursor(LoadCursor(nullptr, IDC_ARROW)); } } // namespace reasampler::panel