From 0484599408857a60d4105bf3f604d6cc82cf6aca Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Fri, 25 Sep 2026 13:52:28 -0400 Subject: [PATCH] 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. --- src/app/CMakeLists.txt | 1 + src/shell/actions/CLAUDE.md | 2 +- src/shell/actions/drag_out_win.cpp | 4 +- src/shell/actions/drag_out_win.h | 14 +++- src/shell/panel/panel_drag.cpp | 99 ++++--------------------- src/shell/panel/panel_drag_os.cpp | 113 +++++++++++++++++++++++++++++ src/shell/panel/panel_state.h | 21 +++++- src/shell/panel/panel_window.cpp | 2 +- 8 files changed, 165 insertions(+), 91 deletions(-) create mode 100644 src/shell/panel/panel_drag_os.cpp diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 1a801c0..a295cce 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -27,6 +27,7 @@ add_library(reaper_reasampler MODULE ${REASAMPLER_SRC_DIR}/shell/panel/panel_audition.cpp ${REASAMPLER_SRC_DIR}/shell/panel/panel_bank_ops.cpp ${REASAMPLER_SRC_DIR}/shell/panel/panel_drag.cpp + ${REASAMPLER_SRC_DIR}/shell/panel/panel_drag_os.cpp ${REASAMPLER_SRC_DIR}/shell/panel/footer_input.cpp ${REASAMPLER_SRC_DIR}/shell/panel/panel_input.cpp ${REASAMPLER_SRC_DIR}/shell/panel/panel_layout.cpp diff --git a/src/shell/actions/CLAUDE.md b/src/shell/actions/CLAUDE.md index 561f479..54cec56 100644 --- a/src/shell/actions/CLAUDE.md +++ b/src/shell/actions/CLAUDE.md @@ -57,7 +57,7 @@ is owned by other directories and only skinned here. - `shell/actions` (`action_registry` / `design_view_actions` / `bank_actions` / `prune_action`) — the bindable action families, all routed via the `command_id`/`gaccel`/`hookcommand` contract. `action_registry` owns the shared registration plumbing (interned channel-qualified id strings; register and mirror-unregister present the identical pointer) **and the Q-W6 registration TABLE**: `main.cpp`'s own family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is one `ActionTableRow` array — suffix, phrase, flat function-pointer handler — that registration, hookcommand dispatch, and the unload mirror-unregister all iterate, so adding an action touches the table only (OCP). Bank mutations flow through the promptless `shell/bank_ops` verbs (`bankOp*` + `persistBankOp`, taking `ReaSamplerSession&`), which the panel menus and `bank_actions` consume as thin UX skins; `src/shell/bank_ops/CLAUDE.md` owns the one-op-one-Ctrl-Z rule those verbs enforce. The prune action (`prune_action`, `BANK_PRUNE_FOLDER`) is **the ONLY file-deletion action in the system**; it opens no undo point (file deletion is not REAPER-undoable). `BANK_PRUNE_FOLDER` halts on `blockedByTracking` and prints each blocker that fired, with recovery instructions. - `package_export_action` — the "export bank as package" skin: survey and report first, confirm what is absent (and, separately, a destination being replaced), pick a destination, write. Every prompt in the flow lives here so `shell/package/export_bank` stays promptless. Read-only against the project — it holds the session by `const&`, so no ext-state write, generation bump or undo point is reachable. Registration rides `main.cpp`'s action table (`EXPORT_BANK_PACKAGE`); the panel's tab menu is the second skin over the same body. -- `drag_out_win` — the host/OS boundary of drag-out: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`. Owns the two window tests behind `ui::DropContext`'s hand-off proofs (`core/ui/CLAUDE.md` owns the rule they feed). `pointerOverHostWindow` is process identity on Windows (`WindowFromPoint` + `GetWindowThreadProcessId`), non-null `WindowFromPoint` under SWELL, whose window list is already this-process-only; its shipped exception: a plugin bridged into `reaper_host*.exe` reports off-host though the pointer never left REAPER. `pointerOverPluginEditor` walks from the hit window to its root reading each window's owning process and the module of its CLASS procedure (`GetClassLongPtr(GCLP_WNDPROC)` → `GetModuleHandleEx(FROM_ADDRESS)`), and lets `ui::window_origin` decide; its failure modes are listed at the declaration, and it is always false under SWELL. The OLE drop source re-asks the gesture law on every `QueryContinueDrag` (a function pointer the panel supplies) and withdraws the drag per `ui::decideOsDragTurn`, reporting it as `OsDragEnd::Withdrawn*` so the panel resumes its own gesture. A drop into a plugin writes nothing to the bank and inserts no timeline item — the plugin receives a copy-only path list, exactly as from Explorer. +- `drag_out_win` — the host/OS boundary of drag-out: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`. Owns the two window tests behind `ui::DropContext`'s hand-off proofs (`core/ui/CLAUDE.md` owns the rule they feed). `pointerOverHostWindow` is process identity on Windows (`WindowFromPoint` + `GetWindowThreadProcessId`), non-null `WindowFromPoint` under SWELL, whose window list is already this-process-only; its shipped exception: a plugin bridged into `reaper_host*.exe` reports off-host though the pointer never left REAPER. `pointerOverPluginEditor` walks from the hit window to its root reading each window's owning process and the module of its CLASS procedure (`GetClassLongPtr(GCLP_WNDPROC)` → `GetModuleHandleEx(FROM_ADDRESS)`), and lets `ui::window_origin` decide; its failure modes are listed at the declaration, and it is always false under SWELL. The OLE drop source re-asks the gesture law on every `QueryContinueDrag` (a function pointer the panel supplies) and withdraws the drag per `ui::decideOsDragTurn`, reporting it as `OsDragEnd::Withdrawn*` so the panel resumes its own gesture. A drop into a plugin means the extension writes nothing to the bank and inserts no timeline item — the plugin receives a copy-only path list, exactly as from Explorer (the plugin's own reaction to that path list is outside the extension's control). - `instrument_drop_win` — instrument-drop shell: `probeDropTarget` resolves a screen point to a track + a `ReaperSurface` (via the pure `wire::classifyReaperSurface`, whose token rules `core/wire/CLAUDE.md` owns) — it answers only what REAPER named, never whether the pointer is still in REAPER or over a plugin's own editor (so an FX surface reaches the instrument drop only as its chrome; a plugin's editor inside it hands off to the OS) — and the drop half adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.** - `insert_fx_action` — the "Insert as FX" verb: a second ENTRY POINT to `instrument_drop_win`'s drop body (the bank panel's Placement button and a bindable action), loading the panel's focused capture onto the first selected track. Owns no insert logic of its own — it resolves the selection, folds `core/ui/insert_fx_enable`, and calls `performInstrumentDrop` (which keeps the one undo block). Refuses visibly on every failing precondition; the panel button additionally paints dead for the two panel-local ones. - `arrange_drop_win` — the drag-out gesture's arrange outcome: `arrangeTimeAtScreenX` (pointer column → time via `GetSet_ArrangeView2`'s one-pixel-span reading — inferred, not SDK-documented) and `performArrangeDrop` (snap the drop time, then one `InsertMedia` per capture on the pointer's track — assumed, not confirmed, to land end-to-end via REAPER's own cursor advance — in ONE undo block, counting only InsertMedia's reported successes, with the caller's track selection and edit cursor restored). The one timeline-placing shell here, per the invariant above; it never captures and never writes the bank. diff --git a/src/shell/actions/drag_out_win.cpp b/src/shell/actions/drag_out_win.cpp index 49a4fe3..e038eec 100644 --- a/src/shell/actions/drag_out_win.cpp +++ b/src/shell/actions/drag_out_win.cpp @@ -356,7 +356,9 @@ ui::WindowFacts windowFacts(HWND hwnd) { } f.procResolved = true; f.hostModule = mod == GetModuleHandleW(nullptr); - f.modulePath = moduleFileName(mod); + // classifyWindowCode returns Host on hostModule before ever reading modulePath — skip the + // (string-building) lookup in that case. + if (!f.hostModule) f.modulePath = moduleFileName(mod); return f; } diff --git a/src/shell/actions/drag_out_win.h b/src/shell/actions/drag_out_win.h index 7129ac7..a85fdbf 100644 --- a/src/shell/actions/drag_out_win.h +++ b/src/shell/actions/drag_out_win.h @@ -64,9 +64,21 @@ bool pointerOverHostWindow(int screenX, int screenY); // - a plugin that draws straight into the parent REAPER hands it, creating no window of its // own, leaves nothing of its code in the chain; // - a plugin binary installed beneath the Windows directory reads as an OS module; -// - a class procedure that resolves to no module (a charset-mismatch handle) is no evidence. +// - a class procedure that resolves to no module (a charset-mismatch handle) is no evidence; +// - an all-#32770 dialog-based editor (the generic Windows dialog class) resolves to no +// recognizable plugin module — falls to REAPER's outcome; +// - a .NET/WPF editor's class procedure is a CLR-thunked address with no resolvable module — +// also falls to REAPER's outcome. +// The opposite direction is unguarded: a third-party hook that replaces REAPER's own chrome +// class procedure via SetClassLongPtr(GCLP_WNDPROC) would make REAPER's own window read as +// Plugin — toward OLE instead of away from it. // [verify — DAW] that REAPER's GetThingFromPoint names an fx_* token over the plugin's own child // window — the law requires it, so an unnamed editor would refuse rather than hand off. +// [verify — DAW] that OLE never walks up to an ancestor drop target (e.g. REAPER's own file +// import) when the plugin editor's window registers no IDropTarget of its own. +// [verify — DAW] that GetThingFromPoint cannot name a REAPER region (e.g. an fx_* token) when a +// foreign window overlaps it at the same screen point — this fn trusts WindowFromPoint's hit +// over whatever GetThingFromPoint separately reports. // SWELL: always false (no plugin-editor hand-off off Windows). Side-effect-free. bool pointerOverPluginEditor(int screenX, int screenY); diff --git a/src/shell/panel/panel_drag.cpp b/src/shell/panel/panel_drag.cpp index a5bb4fe..0b83541 100644 --- a/src/shell/panel/panel_drag.cpp +++ b/src/shell/panel/panel_drag.cpp @@ -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 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); diff --git a/src/shell/panel/panel_drag_os.cpp b/src/shell/panel/panel_drag_os.cpp new file mode 100644 index 0000000..278d4c6 --- /dev/null +++ b/src/shell/panel/panel_drag_os.cpp @@ -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 +#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 diff --git a/src/shell/panel/panel_state.h b/src/shell/panel/panel_state.h index 2a77e79..9926bb9 100644 --- a/src/shell/panel/panel_state.h +++ b/src/shell/panel/panel_state.h @@ -536,11 +536,28 @@ void registerAccel(); void unregisterAccel(); // panel_drag.cpp — the card-drag/hover state machine (pure mirror: core/ui/card_drag). -// Per-mouse-move work stays plain free-function calls. -void onMouseMove(int x, int y); +// Per-mouse-move work stays plain free-function calls. wParam carries WM_MOUSEMOVE's own +// button-state flags (MK_LBUTTON etc) so a move with the button already up can be told apart +// from GetAsyncKeyState's live-polled state. +void onMouseMove(int x, int y, WPARAM wParam); void onLBtnUp(int x, int y); void handleRightClick(int x, int y); void resetDragState(); void maybeShowTooltip(); +// 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"). Shared with panel_drag_os.cpp. +struct LiveDrop { + DropClass cls = DropClass::None; + MediaTrack* track = nullptr; + int screenX = 0; // the evaluated point in screen coords; meaningful only outside the client +}; +LiveDrop resolveLiveDrop(int x, int y); +void applyDropCue(DropCue cue); + +// panel_drag_os.cpp — the OS hand-off / resume seam split out of panel_drag.cpp (the ~600-line +// file ceiling). +void handOffToOs(); + } // namespace reasampler::panel diff --git a/src/shell/panel/panel_window.cpp b/src/shell/panel/panel_window.cpp index edb7e3a..d895a4f 100644 --- a/src/shell/panel/panel_window.cpp +++ b/src/shell/panel/panel_window.cpp @@ -152,7 +152,7 @@ WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { return 0; } case WM_MOUSEMOVE: - onMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + onMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), wParam); return 0; case WM_LBUTTONUP: onLBtnUp(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));