The nvx_kinetix breakthrough
Moving the cursor between two Windows PCs was the easy part. Getting a dragged file to drop into the folder under the cursor on the other machine meant learning which thread Windows lets hold the mouse, after one version froze my desktop.
nvx_kinetix is the KVM I am building because I kept bouncing off the ones that exist: one keyboard and mouse across several Windows machines, meant to hold up next to ShareMouse, Synergy and Deskflow.
Moving the cursor across a screen edge is the part every tool already does, several of them for free. File drag took me longer than everything else in the project put together, and this is most of the reason.
- 01Failed
Inject a press, run
DoDragDropon a worker threadChrome selected text instead of dragging. The release did nothing. When it wedged, the desktop stayed frozen until Task Manager ended the process.
- 02Shelved
Skip OLE and copy into whatever folder is under the cursor
Worked for Explorer folders and the Desktop. Can never drop into a browser or an app.
- 03Shipped
Take the press on a window I own
A real OLE drop into whatever sits under the cursor, with that app’s own highlight.
An empty function called fakeDraggingFiles
Synergy, Barrier and Deskflow come from the same codebase, and all three contain a function named fakeDraggingFiles. It is where the receiving machine would perform the drop. On Windows the body is empty, and the comment above it calls this a possible design flaw.
What those tools do instead is read the dragged file’s path on the source machine, send the bytes over, and write the file into a fixed folder on the target, usually the Desktop through CSIDL_DESKTOP. You let go over a project folder, the file turns up on the Desktop, and you pick it up again and drag it where you meant.
ShareMouse drops files properly, and you pay for it. I wanted that behaviour in an open-core tool: the file goes to whatever is under the cursor on the second machine, whether that is an Explorer folder, the Desktop, a browser or any other app that accepts files, and that app draws its own drop highlight, on whichever monitor the cursor is on.
How Windows decides where a drop goes
Drag and drop on Windows is OLE, built on three COM interfaces.
IDataObjectis the payload. For files that means theCF_HDROPformat, a list of paths.IDropSourcebelongs to whoever started the drag.QueryContinueDragis asked on every tick whether to keep going, drop or cancel, andGiveFeedbacksets the cursor.IDropTargetis implemented by any window that accepts drops and registered privately withRegisterDragDrop. The source never sees it.
DoDragDrop ties them together, and it does not return until the drag is over. It creates a hidden tracker window, takes the mouse with SetCapture, and runs its own GetMessage loop. On each mouse move it calls WindowFromPoint, walks up to the nearest registered target, and calls DragEnter or DragOver on it. On button-up it calls Drop on whatever is under the cursor, then returns.
Two details in that loop matter for everything below. It holds the mouse capture for as long as it runs, and the drop goes to the window under the cursor, never to the code that called it.
Faking the press
The first version did what looked obvious. When the cursor crossed onto the second machine carrying a file, inject a left button-down with SendInput, then call DoDragDrop on a background thread with my own data object. Forward the mouse moves into it, and when the button came up on the source machine, inject a button-up to finish the drop.
On real hardware, dragging over a browser selected text. The release did not drop anything. And when the drag wedged, the whole desktop stopped responding until I ended the process from Task Manager.
A cancel flag the loop never read
The freeze was the easier one to explain. DoDragDrop holds the mouse capture inside a modal loop, so if input stops reaching that loop it waits indefinitely, still holding the mouse, and on Windows that looks exactly like a hung machine.
I had a safety net for this: a cancel flag checked inside QueryContinueDrag. But QueryContinueDrag only runs when input reaches the loop, which was the thing not happening. My watchdog was setting that flag thousands of times a second, and the loop never once looked at it.
What does get through is more input. An injected mouse move reaches the captured tracker window no matter which window has focus, and so does an injected Escape. Either one ends the loop.
That explained the freeze. The text selection was still unexplained, and it was the more useful clue.
The version that cannot freeze
By this point the sensible move looked like giving up on OLE. Send the file, draw a thumbnail that follows the cursor, and on release find the folder under the cursor and copy the file into it with the shell. No modal loop and no mouse capture, so nothing left that could hang.
I built it, and it worked for Explorer folders and the Desktop.
| Shell copy | OLE drop | |
|---|---|---|
| Explorer folder under the cursor | ||
| The Desktop | ||
| A browser | ||
| Any other app that accepts files | ||
| The target draws its own highlight | ||
| Risk of hanging the desktop | nothing to hang | holds the mouse while it runs |
The browser and app rows were the ones I could not live with. Only a real OLE drop can land there, so shipping the copy version meant shipping Synergy’s workaround with a nicer thumbnail. It went in a drawer, and I went back to the text selection.
Who got the click
Windows only lets certain threads take the mouse capture. A thread becomes eligible when a mouse button goes down over one of its own windows, and it keeps an implicit capture until that button comes back up. DoDragDrop’s tracker window can only take the capture on an eligible thread.
My injected press went to whatever was under the cursor, which was Chrome. Chrome’s thread got the WM_LBUTTONDOWN, and the implicit capture with it. My worker thread then called DoDragDrop, and the tracker’s SetCapture was refused, because another process already had the mouse. The drag never received a single input event. Chrome saw a press followed by movement, handled it as an ordinary click-drag, and selected text, while my modal loop sat behind it waiting.
- 01
SendInputbutton-down arrives asWM_LBUTTONDOWN - 02takes the implicit mouse capture
- 03
DoDragDrop(dataObject, dropSource, …) - 04
SetCapturerefused: this thread never got the press - 05forwarded mouse moves
- 06press plus movement, so it selects text
- 07
GetMessagewaits for input that never comes
- 01nvx_kinetixChrome
SendInputbutton-down arrives asWM_LBUTTONDOWN - 02Chrometakes the implicit mouse capture
- 03WorkerOLE tracker
DoDragDrop(dataObject, dropSource, …) - 04OLE tracker
SetCapturerefused: this thread never got the press - 05nvx_kinetixChromeforwarded mouse moves
- 06Chromepress plus movement, so it selects text
- 07OLE tracker
GetMessagewaits for input that never comes
Every input event went to Chrome. The drag’s loop started on a thread with no claim to the mouse.
“Injecting a press into someone else’s window will never start your own drag. The drag has to run on the thread that received the press.”
A window at alpha 1
So the press has to land on a window I own. When the cursor arrives on the target machine with a file pending, nvx_kinetix puts a tiny top-most window underneath it, layered at alpha 1. At alpha 1 nobody can see it, but it is still fully hit-testable. A transparent click-through window would be skipped by the hit test, and being hit is its only job.
Then comes the injected button-down. My window is the top-most thing under the cursor, so it receives the WM_LBUTTONDOWN, and my thread now holds the implicit capture. The rest happens synchronously, inside that message handler.
1case WM_LBUTTONDOWN:
2{
3 SetWindowPos(hwnd, nullptr, -32000, -32000, 0, 0,
4 SWP_NOSIZE | SWP_NOZORDER);
5
6 DWORD effect = DROPEFFECT_NONE;
7 DoDragDrop(dataObject, dropSource, DROPEFFECT_COPY, &effect);
8 return 0;
9}- 1L3
DoDragDrophit-tests withWindowFromPointon every move. Left under the cursor, this window is the only thing it would ever find, and the real target would never getDragEnter. - 2L4Moved, not hidden. Hiding a window that holds the mouse capture releases the capture.
- 3L7Same thread that received the press, so
SetCaptureinside the tracker succeeds. Blocks until the drop.
- 01
SendInputbutton-down arrives asWM_LBUTTONDOWN - 02the implicit capture belongs to my thread
- 03moves itself to -32000, -32000
- 04
DoDragDropon the same thread - 05
SetCapturesucceeds - 06forwarded mouse moves
- 07
DragEnter, thenDragOver, and the target draws its own highlight - 08forwarded release
- 09
Drop
- 01nvx_kinetixAlpha-1 window
SendInputbutton-down arrives asWM_LBUTTONDOWN - 02Alpha-1 windowthe implicit capture belongs to my thread
- 03Alpha-1 windowmoves itself to -32000, -32000
- 04Alpha-1 windowOLE tracker
DoDragDropon the same thread - 05OLE tracker
SetCapturesucceeds - 06nvx_kinetixOLE trackerforwarded mouse moves
- 07OLE trackerDrop target
DragEnter, thenDragOver, and the target draws its own highlight - 08nvx_kinetixOLE trackerforwarded release
- 09OLE trackerDrop target
Drop
After that first press, the little window has nothing left to do. Forwarded moves drive DragOver on the real window under the cursor, and the forwarded release drops the file into the folder, the Desktop or the browser beneath it.
The source machine does the same thing in reverse. A thin invisible strip along the screen edge is registered as an IDropTarget, and as the cursor carries a drag across it, the strip reads the CF_HDROP out and sends the file list to the other machine.
Escape, not a button-up
One more bug kept eating drops. After the edge strip reads the file list, the source machine’s own drag has to be cancelled so the file does not also drop there. I was cancelling it with an Escape followed by a synthetic button-up.
But the user is still physically holding the button at that moment. The synthetic up told Windows it had been released, so when the finger really lifted there was no change in state, no event, and nothing to forward. Escape on its own ends DoDragDrop without dropping and leaves the button down, so the real release survives long enough to reach the other machine.
Scaled displays and shutdown
The drag-source thread has to declare PER_MONITOR_AWARE_V2. Without it, on a scaled display, DoDragDrop tracks the cursor in virtualized coordinates and the drop lands on the wrong monitor. Every thread that deals in physical pixels has to agree on this: capture, injection, the edge strip, the drag source and the thumbnail overlay.
A stuck drag also must not stop the app from quitting. On shutdown nvx_kinetix requests a cancel, then detaches the drag thread instead of joining it. Process teardown kills the thread and releases any capture it held.
Dragging from one PC into Chrome on another
Drag a file off the edge of one machine, let go over a Chrome window on the other, and it drops into Chrome. The same goes for an Explorer folder, the Desktop, or any app that accepts files, on any monitor, with the target’s own highlight, under the real mouse the whole way. I have not found another open-core tool in this category that does this on Windows.
The file bytes travel over QUIC with device-key pinning, which needs a write-up of its own.
Admin windows
One case still does not work. Windows UIPI blocks synthetic input from a medium-integrity process into a higher-integrity window, so the injected press and release never reach an app running as administrator. Running nvx_kinetix elevated gets around it, and every tool in this space runs into the same wall.
Next on the list: baking a drop-description badge into the drag image, and letting drops target folders in Explorer’s navigation pane.