So I've had this idea for an AutoHotKey script that could turn motions from any pointing device (mice, trackballs, trackpads, etc.) into custom actions.
I guess you might call them gestures?
Aside: for purposes of this post, I'll often write "mouse" in context of system handling, when in reality it's anything that moves the cursor around - that includes "regular" mice, trackballs, trackpads, trackpoints, pens, touchscreens, and a variety of devices asserting that they can be a mouse (e.g. many programmable keyboards can also send mouse events).
Inspiration
On the right of my keyboard, I have a mouse that's my primary pointing device.
On the left of my keyboard, I have a Kensington Orbit trackball.
A diagram of Kensington Orbit trackball
Orbit could not fully replace a mouse for my uses, but at some point I noticed that it has a "track scroll" mode that you can bind to one or combination of buttons.
The said "track scroll" mode makes the ball act as a mouse wheel until one of the buttons is pressed again. And it's really nice - rolling a ball is a far more convenient way to scroll than most mouse wheels that I've used.
This also got me thinking - the driver maps horizontal motions to horizontal scrolling when "track scroll" is active, but there really isn't much software where horizontal scrolling is a significant part of workflow (mostly image editors, really).
What would it feel like if I could map the second axis to tab switching, arrow keys, volume control, or something else?
Research
As far as I can tell, you can't really "intercept" input from a specific mouse on Windows - at least not without writing a driver, which is a sort of trouble that I'd rather not get into.
However, a few years ago I already messed around with Raw Input API (and even made a GameMaker wrapper) and know how Windows handles input at lower level.
On Windows, inputs from connected devices kind of fight over the global state: mice offset the cursor, pens and touchscreens overwrite its position, and funny things may happen if you're pressing buttons on multiple devices at once.
Raw Input API lets you peek behind this curtain - you get events with per-device readings with coordinate changes and pressed/released buttons, even for changes that would not be observable (e.g. when the cursor is rubbing against edges of the screen).
My first idea was to undo movements from the chosen device(s) right in the Raw Input (WM_INPUT
) handler.
This didn't work very well - Raw Input is a low-level API, but my C++ test program still could not rollback the cursor fast enough for it to not wobble back and forth.
Thus I settled for a workaround - when Raw Input reports mouse movement and conditions are satisfied (e.g. it's the desired mouse), I lock the mouse cursor in place and unlock it once enough time has passed without matching movements.
This means that you can't move the cursor (with another mouse) at the exact moment of scrolling, but with unlock delay of 30ms or less it's not much of an inconvenience.
Later I found another caveat - you can specify a "delta" for mouse scroll events yourself (the usual being -120 or 120), but a lot of software doesn't handle low-delta events very well, if at all.
So I changed the code to dispatch one "regular" scroll event once per N pixels of movement instead.
Implementation
With my shabby C++ test program now successfully scrolling the wheel when a trackball is rolled, it was finally time to rewrite it in something that's easier to customize without digging into WinAPI.
For Windows, I think that's currently AutoHotKey. AHKv1 has been a little weird with its layers of syntactic constructs piled on top of the old ones, but AHKv2 fixed most of those oddities.
With a little work (changing my WinAPI calls to DllCall
) and some contemplation about what the API should look like, I finished the AHK version, wrote a little cheat sheet for it, and that's it.
I also updated my cheat sheet preprocessor to display "sticky" section names on the left:
Experiments
With the AHK version done, I could finally get to figuring out whether my alternate trackball use ideas were any good or not.
The results are as following:
Scrolling
I already mentioned this one, feels great on a trackball.
On a regular mouse, this feels convenient, but stranger.
Volume control
Also good, generally on par with rotary encoders.
Arrow keys
This one's interesting - rolling a ball to move the caret offers more control over caret speed, and this could potentially eliminate much of the need for the navigation block, but there are some challenges.
I was able to overcome accidental secondary-axis input by introducing options to consider one axis at a time and to temporarily disable movement from another axis after scrolling for long enough on one of them.
The default behaviour in text/code editors is that pressing Right while at the end of the line wraps over to the next line, which is already a little weird for code (as you probably have indentation there) and weirder if you're trying to brake in time with your rolling.
On a regular mouse, mapping movement to arrow keys seems mostly-redundant as you can usually just click the spot.
Conclusions
To me, this was an interesting thing to do between other tasks.
You can find the script and its examples on GitHub.
The cheat sheet is hosted on GitHub Pages.
Have fun!
Hi Vadym. Thankyou so much for publishing your utility. It works very well for me as a solution since my scrollwheel stopped working. I can now hold down LAlt to engage scrolling, using:
sb.config.conditions := ScrollBall.KeyHoldConditions(“LAlt”, true)
First I have a correction on your cheat sheet: for the “Y” axis description you have “Controls how X-Axis works” (copied and pasted from the “X” axis description, no doubt).
Now a couple of queries. Firstly, you mentioned that some windows can’t handle deltas less than 120 pixels. I wanted to try smaller numbers in case the app I use this for is OK with smaller pixels, but I couldn’t find where to set this. pixelsPerStep doesn’t seem to make a difference.
Secondly, when using “KeyHoldConditions” as per above, my mouse pointer slowly slides in the opposite direction while the LAlt key is held down. So if I pull my mouse down, towards me, the mouse pointer moves upwards on the page, eventually leaving the window and starts to scroll any other window that happens to be behind. Is there a setting to offset this drift?
If I use ManualStart and enable/disable scrollwheel mode using “LAlt:: sb.Start()” and “LAlt Up:: sb.Stop()” then the mouse pointer correctly stays still. However with this, I can’t use LAlt for other things like Alt-Esc to switch windows. I tried re-injecting the LAlt key but couldn’t get it to work reliably, and I think the Alt key sometimes got stuck down.
If I start/stop using Alt-S like in your example script, the pointer also correctly stays still, but a two-key combination makes it more effort to activate/deactivate, and when I switch modes often (for doing 3D modelling) it’s much better if I can use a single Alt (or Ctrl or Shift) key press to activate, and release to deactivate.
The second argument in WheelScroller is the delta, you can use it like so:
pixelsPerStep is the number of pixels that the mouse needs to move for one scroller action.
I’m not seeing any drift when using LAlt as activator, but probably means that the cursor is getting unlocked during scrolling. You can try adding a trace(“start”) call to Start() function in ScrollBall.ahk to test and increase config.unlockDelay if necessary.
LAlt is a somewhat regrettable key in general – e.g. Firefox does Forward/Backward navigation on Alt+wheel. You could remap your Caps Lock to be F13/etc. using PowerToys or SharpKeys and use that instead.
For manual start, you might want to use SetTimer and check for GetKeyState in there – this way you won’t be consuming the key press.
I’ll fix the doc later.
Hi YA,
Love your work with GM. I was surprised to come across this as it almost exactly matches my use case – I have been trying to figure out how to use a trackball as a mouse wheel using a ‘toggle’, instead of other common implementations (hold down a button), mainly because of slowly encroaching RSI issues.
That said, I confess I have no idea how to implement this after looking at the git. I have AHK installed but don’t really know what to do with these files. Do I need to run scrollball.ahk and trace.ahk and then make my own ahk as well? Do I need to rebind a specific key to fire the sb.listen()? I am fairly unfamiliar with AHK other than the flat basics of rebinding keys to other keys, so any help is deeply appreciated.
Hello,
There’s a pile of examples in the repository. For example,
example - simple.ahk
just scrolls. Other examples show how to configure the system as per documentation.thanks, i will give it a shot!
Thanks, I got it working after properly perusing the documentation and reading up a bit more on AHK. Hopefully this will save my fingers. Cheers!