If you use a touch-enabled display on Windows, you might have noticed a certain quirk: when you tap on the touchscreen, Windows might move your cursor to touch location, but will not put it back afterwards, which is an inconvenience if it's a smaller secondary display that you use for macros/etc. or even if you are using mouse/touch interchangeably.
So I wrote an AutoHotKey macro that fixes this.
A little context
During the past year I've been using SteamDeck for work when blackouts were common - usually it would sit in its little dock below a large display, like so:
And that's a neat setup because moving your eyes down is quicker than turning your head to look at a side monitor, but also because you can touch the handheld's screen to focus a chat field or swipe to scroll a to-do list.
Recently I thought that it'd be fun to have something similar going for my desktop computer, so after doing some measurements I got a 14" multi-touch monitor:
The mini-monitor in question (Verbatim PMT-14) is a curious gadget - you can connect it to laptops and mobile devices with USB-C cable, or you can connect it to regular desktop PCs with a HDMI cable for video and a USB-C ↔ USB-A cable for touch input.
This kind of dual-cable setup also means that Windows does not know which display is the touch display and you have to tell it by going in to Control Panel ➜ Tablet PC Settings ➜ Setup.
With that out of the way,
What's going on, anyway
The way touch input works on Windows is as following: if the window handles the pointer events, the cursor will stay where it was. If it doesn't, Windows will move the cursor and emit a regular mouse event so that old and/or touch-unaware apps are less likely to break.
This makes sense, but is also inconvenient to the end user because:
- You cannot tell on a glance whether an application handles touch input.
- The mouse cursor is hidden after touching the screen
And thus it can be hard to tell where your cursor will end up after touch without wiggling the mouse and checking both screens. Not the best, isn't it
The macro
This AutoHotKey v2 macro runs a timer to check that the mouse jumped onto the touchscreen display too quickly and moves the cursor to the previous position once the mouse button(s) have been released.
#SingleInstance Force class TouchscreenCursor { mouseX := 0 mouseY := 0 mouseBuf := Buffer(8) lockTimer := 0 ; zoneLeft := 0 zoneTop := 0 zoneRight := 0 zoneBottom := 0 ; updateFrequency := 10 jumpThreshold := 100 unlockAfter := 20 ; distance(x1, y1, x2, y2) { dx := x2 - x1 dy := y2 - y1 return Sqrt(dx * dx + dy * dy) } ; creates circle tray icons createIcon(color) { size := 16 pixels := Buffer(size * size * 4, 0) loop 16 { y := A_Index - 1 loop 16 { x := A_Index - 1 dist := this.distance(x, y, 7.5, 7.5) alpha := 1 - (dist - 6) / 1.5 if (alpha > 0) { rgba := (Round(Min(alpha, 1) * 255) << 24) | color NumPut("UInt", rgba, pixels, 4 * ((y * size) + x)) } } } return "HBITMAP:*" DllCall("CreateBitmap", "Int", 16, "Int", 16, "Uint", 1, "Uint", 32, "Ptr", pixels, "Ptr" ) } resetIcon() { TraySetIcon(this.iconOff) SetTimer(this.resetIcon_t, 0) } resetIcon_t := this.resetIcon.Bind(this) ; inZone(mx, my) { return mx >= this.zoneLeft and mx < this.zoneLeft + this.zoneRight and my >= this.zoneTop and my < this.zoneTop + this.zoneBottom } setZone(left, top, right, bottom) { this.zoneLeft := left this.zoneRight := right this.zoneTop := top this.zoneBottom := bottom A_IconTip := ("tc.setZone(" this.zoneLeft ", " this.zoneTop ", " this.zoneRight ", " this.zoneBottom ")") } setZoneAuto() { DllCall("GetCursorPos", "Ptr", this.mouseBuf) mx := NumGet(this.mouseBuf, 0, "Int") my := NumGet(this.mouseBuf, 4, "Int") loop MonitorGetCount() { MonitorGet(A_Index, &monLeft, &monTop, &monRight, &monBottom) if (mx >= monLeft && mx < monRight && my >= monTop && my <= monBottom) { this.setZone(monLeft, monTop, monRight, monBottom) return true } } return false } update() { if (this.lockTimer > 0) { if (GetKeyState("LButton") or GetKeyState("RButton") or GetKeyState("MButton")) { ; dragging a window, etc. - please wait } else { this.lockTimer -= this.updateFrequency if (this.lockTimer <= 0) { DllCall("SetCursorPos", "int", this.mouseX, "int", this.mouseY) SetTimer(this.resetIcon_t, 150) } } } else { DllCall("GetCursorPos", "Ptr", this.mouseBuf) newX := NumGet(this.mouseBuf, 0, "Int") newY := NumGet(this.mouseBuf, 4, "Int") if (this.inZone(newX, newY) and not this.inZone(this.mouseX, this.mouseY) and this.distance(this.mouseX, this.mouseY, newX, newY) > this.jumpThreshold ) { this.lockTimer := this.unlockAfter TraySetIcon(this.iconOn) SetTimer(this.resetIcon_t, 0) } else { this.mouseX := newX this.mouseY := newY } } } ; __New() { this.iconOff := this.createIcon(0x707E98) this.iconOn := this.createIcon(0x43D3E0) ; 38C493 for minty color this.resetIcon() } listen(frequency, jumpThreshold) { DllCall("GetCursorPos", "Ptr", this.mouseBuf) this.mouseX := NumGet(this.mouseBuf, 0, "Int") this.mouseY := NumGet(this.mouseBuf, 4, "Int") this.jumpThreshold := jumpThreshold this.updateFrequency := frequency SetTimer(this.Update.Bind(this), frequency) } } tc := TouchscreenCursor() tc.setZoneAuto() tc.listen(10, 200)
You can save the above to an .ahk
file, edit it as you please, and run it.
If you run it without changing anything, it'll assume the touchscreen monitor to be the one where the mouse cursor is at the time of launching, and you'll get this circular tray icon:
The icon will blink whenever the tool reverts mouse movement, and if you hold the mouse over it, it'll display the monitor bounds so that you can change
tc.setZoneAuto()
to something like
tc.setZone(320, 1440, 2240, 2340)
and not have position your cursor each time.
The arguments of tc.listen
are how often the macro should poll for mouse position
(in milliseconds) and how far the cursor should move over one such interval (in pixels)
for it to be considered a touch-induced snap.
The defaults should work well.
You can adjust the tray icon colors in the __New
function.
As a certain caveat, this macro has no way of tell apart other software-induced mouse movements, like Mouse Jump in PowerToys.
Thanks for reading!