Fix stuck drag after OS hand-off withdrawal, split panel_drag_os, doc fixes

Re-checks the physical button before resuming an internal drag and treats a button-up move mid-drag as a release, closing a stale-payload/silent-drop bug; splits the OS hand-off seam into its own TU for the line ceiling; corrects two docs and skips a redundant module-path lookup.
This commit is contained in:
2026-09-25 13:52:28 -04:00
parent fe1aec5925
commit 0484599408
8 changed files with 165 additions and 91 deletions
+113
View File
@@ -0,0 +1,113 @@
// panel_drag_os.cpp — the OS hand-off / resume seam split out of panel_drag.cpp (the ~600-line
// file ceiling): 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 <string>
#include <vector>
#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<std::string> 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