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:
@@ -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
|
||||
Reference in New Issue
Block a user