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:
@@ -148,11 +148,13 @@ void applyDragCursor(CardGesture g) {
|
||||
SetCursor(LoadCursor(nullptr, idc));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The out-of-client half of the same thin lookup: pure class -> pure cue -> stock cursor. The
|
||||
// cue is set on EVERY move, so "will this work" is visible before the button comes up, and a
|
||||
// refusal is a cursor the user can see rather than a release that does nothing. Cursor-only by
|
||||
// design — the docked panel is usually not under the pointer during an out-of-client drag, so
|
||||
// panel status text would be invisible exactly when it is needed.
|
||||
// panel status text would be invisible exactly when it is needed. Shared with panel_drag_os.cpp.
|
||||
void applyDropCue(DropCue cue) {
|
||||
const char* idc = nullptr;
|
||||
switch (cue) {
|
||||
@@ -169,15 +171,7 @@ void applyDropCue(DropCue cue) {
|
||||
SetCursor(LoadCursor(nullptr, idc));
|
||||
}
|
||||
|
||||
// One independent evaluation of the drag's target: the resolved class plus the live facts its
|
||||
// outcome needs. Nothing is remembered between calls (core/ui/CLAUDE.md: "the drag-out law is
|
||||
// per-move and stateless").
|
||||
struct LiveDrop {
|
||||
DropClass cls = DropClass::None;
|
||||
MediaTrack* track = nullptr;
|
||||
int screenX = 0; // the evaluated point in screen coords; meaningful only outside the client
|
||||
};
|
||||
|
||||
// Shared with panel_drag_os.cpp (lawHandsOffAtCursor).
|
||||
LiveDrop resolveLiveDrop(int x, int y) {
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
@@ -220,80 +214,7 @@ LiveDrop resolveLiveDrop(int x, int y) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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 {
|
||||
|
||||
// Resolves the topmost INTERACTIVE element under client (x, y) for hover feedback,
|
||||
// mirroring handleClick's precedence exactly (so the element that lights on hover is
|
||||
@@ -381,7 +302,15 @@ void maybeShowTooltip() {
|
||||
}
|
||||
}
|
||||
|
||||
void onMouseMove(int x, int y) {
|
||||
void onMouseMove(int x, int y, WPARAM wParam) {
|
||||
// A move with the button already up while still dragging means the release was missed (the
|
||||
// OS hand-off's own modal loop can eat a WM_LBUTTONUP, and coalescing can lose one too) —
|
||||
// treat it as the release now rather than leaving dragging stuck for the next plain click.
|
||||
if (g_panel.dragging && !(wParam & MK_LBUTTON)) {
|
||||
onLBtnUp(x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
// Hover feedback: resolve + repaint-on-change, but NOT during a drag (the drag owns
|
||||
// the visual feedback then — a drop-target highlight, not a hover).
|
||||
if (!g_panel.dragging && !g_panel.dragArmed) updateHover(x, y);
|
||||
|
||||
Reference in New Issue
Block a user