├── .gitignore
├── LICENSE
├── README.md
├── changelog.txt
├── rollmouse.ahk
├── rollmouse.au.txt
├── rollmouse.exe
└── rollmouse.png
/.gitignore:
--------------------------------------------------------------------------------
1 | *.ini
2 | *.exe
3 | *.zip
4 | *.lnk
5 | *.scc
6 | *.png
7 | *.tmp.*
8 | Setup.ahk
9 | !Setup*.exe
10 | !*.Demo*.exe
11 | !rollmouse.png
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Clive Galway
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RollMouse
2 |
3 | 
4 |
5 | ## What does it do?
6 |
7 | RollMouse is intended to solve the issue of having to compromise between low sensitivity (High accuracy, but hard to generate large mouse movements) and high sensitivity (Can generate large mouse movements easily, but accuracy suffers).
8 |
9 | It does this by making the mouse behave in a similar manner to spinning a TrackBall.
10 |
11 | With RollMouse, you can set your sensitivity low, but still easily generate large and continuous mouse movements.
12 |
13 | RollMouse is compatible with all optical mice (ie most "normal" mice on the market) and laptop trackpads.
14 |
15 | ## Why would I want it?
16 | ### Games
17 | Many mouse have "DPI Shift" / "Sniper Mode" buttons, but these often require sacrificing a button on your mouse in order to use them, and are often impractical to use. Setting your mouse to drop DPI while you hold the aim button is probably the most practical, but shifting DPI mid-game is not going to help your Muscle Memory.
18 | ### Windows
19 | If you have a large desktop area (ie Multiple Monitors), moving the mouse around can be a chore.
20 | If you use a laptop with a trackpad, you probably hate having to make lots of small movements to generate a long movement in one direction.
21 | *Note: If you use RollMouse on a laptop, I strongly recommend also turning on "Pointer Trails" else it can be hard to keep track of the mouse pointer when RollMouse moves it. This option can be enabled by going to Control Panel > Mouse > Pointer Options tab > Display Pointer Trails.*
22 |
23 | ## How do I use it?
24 | First off, some definitions, or this will get confusing ;)
25 | With a mouse, the "surface" is the mouse mat, and the "device" is the mouse.
26 | With a trackpad, the "surface" is the trackpad, and the "device" is your finger.
27 |
28 | If you keep the device in contact with the surface, RollMouse does nothing - it should not interfere with "normal" operation.
29 | However, if you lift the device from the surface **while the device is still in motion** then RollMouse will keep moving the mouse pointer in the direction of the motion until you place the device back on the surface.
30 |
31 | When it does this, the direction and speed that it moves the mouse is proportionate to the speed and direction that you were moving the mouse when you lifted.
32 |
33 | Use of RollMouse is very intuitive - many people already lift while moving, in order to reposition the mouse when it reaches the edge of the mat.
34 | With RollMouse, however, the mouse cursor **keeps moving** while you are repositioning the mouse.
35 |
36 | ## How does it work?
37 | RollMouse makes use of the laws of physics.
38 | If you move a mouse across a surface, no matter how quickly you stop moving the mouse, the movement will "tail off" - ie you start off moving fast and the mouse is sending "5, 5 , 5, 5, [...]".
39 | You then stop, and the mouse will report like "5, 4, 3, 2, 1, 0"
40 | You cannot avoid this - the laws of intertia mean you cannot stop an object with mass instantly.
41 |
42 | However, if you lift the device off the surface whilst in motion, as soon as the mouse reaches a certain height, the sensor stops getting any readings at all - so the mouse will report like "5, 5, 5, 5, 0"
43 |
44 | Thus, RollMouse can detect the difference between a normal move and a "flick and lift" gesture.
45 |
46 | ## How do I run it?
47 | Download RollMouse.exe from the [releases page](https://github.com/evilC/RollMouse/releases) and run it - it's as simple as that.
48 | There is also a source code version (RollMouse.ahk) which you would need AutoHotkey installed (Plus a library) to use.
49 |
--------------------------------------------------------------------------------
/changelog.txt:
--------------------------------------------------------------------------------
1 | 1.0.0 - 24th June 2015
2 |
3 | 1.0.1 - 24th June 2015
4 | + Fix for Minimize On Start option breaking roll.
5 | + Improvement of mouse_event call, as per suggestion by jNizM - thank you!
6 |
7 | 1.0.2 - 25th June 2015
8 | + Fix incorrect datatype for hRawInput callin GetRawInputData
9 |
10 | 1.0.3 - 28th June 2015
11 | + No longer tries to run as admin.
12 | + Now minimizes to tray.
13 |
14 | 1.0.4 - 12th July 2015
15 | + No functionality changes, just added homepage link and update notifications.
16 |
17 | 1.0.5 - 25th Jul 2015
18 | + Now ignores button and wheel activity.
19 |
20 | 1.0.6 - 8th Aug 2016
21 | + Uses better technique for filtering output from input.
22 |
23 | 1.0.7 - 29th Aug 2016
24 | + Functionality toggle binding now enables/disables RollMouse
25 |
26 | 1.0.8 - 23rd Nov 2017
27 | + Added "Friction" option to make Rolls tail off
28 |
29 | 1.0.9 - 17th Nov 2024
30 | + Point auto-update to Github
31 | + Recompile for AHK 1.1.37.2
--------------------------------------------------------------------------------
/rollmouse.ahk:
--------------------------------------------------------------------------------
1 | ; Requires AHK >= 1.1.21.00
2 |
3 | /*
4 | ToDo:
5 |
6 | * Better history.
7 | Expire items that are too old.
8 | Filter outliers - eg a slight move up sometimes has a few move downs in there - keep general up motion but filter out direction inversions.
9 | Clear history on change of direction?
10 |
11 | */
12 |
13 | #SingleInstance force
14 | global ADHD := new ADHDLib
15 |
16 | ADHD.config_about({name: "Rollmouse", version: "1.0.9", author: "evilC", link: "GitHub Page / Discussion Thread"})
17 | ADHD.config_updates("https://raw.githubusercontent.com/evilC/RollMouse/refs/heads/master/rollmouse.au.txt")
18 |
19 | ADHD.config_size(375,230)
20 |
21 | ADHD.config_hotkey_add({uiname: "Quit", subroutine: "Quit"})
22 |
23 | ADHD.config_event("option_changed", "option_changed_hook")
24 | ADHD.config_event("functionality_toggled", "functionality_toggle_hook")
25 |
26 | ADHD.init()
27 | ADHD.create_gui()
28 |
29 | Gui1 := WinExist()
30 |
31 | Gui, Tab, 1
32 |
33 | row := 40
34 |
35 | Gui, Font, italic
36 | Gui, Add, Text, x10 y%row%, Move Factor controls how fast the mouse will be moved in any given`ndirection when you perform a roll. Decimals (eg 0.5) are permissible.
37 | Gui, Font
38 | row += 30
39 |
40 | Gui, Add, Text, x10 y%row%, Move Factor:
41 | Gui, Add, Text, x120 yp, x
42 | ADHD.gui_add("Edit", "MoveFactorX", "xp+10 yp-2 W50", "", "1")
43 | Gui, Add, Text, xp+80 yp+2, y
44 | ADHD.gui_add("Edit", "MoveFactorY", "xp+10 yp-2 W50", "", "1")
45 |
46 | row += 30
47 | Gui, Font, italic
48 | Gui, Add, Text, x10 y%row%, Move Threshold controls how fast you have to move the mouse`nto perform a roll. Decimals are NOT permitted.
49 | Gui, Font
50 | row += 30
51 | Gui, Add, Text, x10 y%row%, Move Threshold:
52 | Gui, Add, Text, x120 yp, x
53 | ADHD.gui_add("Edit", "MoveThreshX", "xp+10 yp-2 W50", "", "4")
54 | Gui, Add, Text, xp+80 yp+2, y
55 | ADHD.gui_add("Edit", "MoveThreshY", "xp+10 yp-2 W50", "", "4")
56 |
57 | row += 30
58 | ADHD.gui_add("CheckBox", "MinimizeOnStart", "x10 y" row + 3, "Minimize on StartUp", 0)
59 | Gui, Add, Text, % "x+30 y" row + 3, Friction
60 | ADHD.gui_add("Edit", "Friction", "x+5 y" row, "Friction", 0)
61 |
62 | ADHD.finish_startup()
63 |
64 | global rm := new RollMouse
65 |
66 | Gui1 := WinExist()
67 | Menu("Tray","Nostandard"), Menu("Tray","Add","Restore","GuiShow"), Menu("Tray","Add")
68 | Menu("Tray","Default","Restore"), Menu("Tray","Click",1), Menu("Tray","Standard")
69 |
70 | OnMessage(0x112, "WM_SYSCOMMAND")
71 |
72 | if (MinimizeOnStart){
73 | Gosub, OnMinimizeButton
74 | }
75 |
76 | option_changed_hook()
77 |
78 | ;OutputDebug, DBGVIEWCLEAR
79 |
80 | option_changed_hook(){
81 | global MoveFactorX, MoveFactorY, MoveThreshX, MoveThreshY, Friction
82 | rm.MoveFactor.x := MoveFactorX
83 | rm.MoveFactor.y := MoveFactorY
84 | rm.MoveThreshold.x := MoveThreshX
85 | rm.MoveThreshold.y := MoveThreshY
86 | rm.Friction := Friction
87 | }
88 |
89 | functionality_toggle_hook(){
90 | rm.ListenForMouseMovement(ADHD.private.functionality_enabled)
91 | }
92 |
93 | return
94 |
95 | class RollMouse {
96 | ; User configurable items
97 | ; The speed at which you must move the mouse to be able to trigger a roll
98 | MoveThreshold := {x: 4, y: 4}
99 | ; Good value for my mouse with FPS games: 4
100 | ; Good value for my laptop trackpad: 3
101 |
102 | ; The speed at which to move the mouse, can be decimals (eg 0.5)
103 | ; X and Y do not need to be equal
104 | ; Good value for my mouse with FPS games: x:2, y: 1 (don't need vertical roll so much)
105 | MoveFactor := {x: 1, y: 1}
106 | ; Good value for my laptop trackpad: 0.2
107 |
108 | ; How fast (in ms) to send moves when rolling.
109 | ; High values for this will cause rolls to appear jerky instead of smooth
110 | ; if you halved this, double MoveFactor to get the same move amount, but at a faster frequency.
111 | RollFreq := 1
112 |
113 | ; How long to wait after each move to decide whether a roll has taken place.
114 | TimeOutRate := 50
115 |
116 | ; The amount that we are currently rolling by
117 | LastMove := {x: 0, y: 0}
118 |
119 | ; The number of previous moves stored - used to calculate vector of a roll
120 | ; Higher numbers = greater fidelity, but more CPU
121 | MOVE_BUFFER_SIZE := 5
122 |
123 | ; Non user-configurable items
124 | STATE_UNDER_THRESH := 1
125 | STATE_OVER_THRESH := 2
126 | STATE_ROLLING := 3
127 | StateNames := ["UNDER THRESHOLD", "OVER THRESHOLD", "ROLLING"]
128 |
129 | State := 1
130 |
131 | TimeOutFunc := 0
132 | History := {} ; Movement history. The most recent item is first (Index 1), and old (high index) items get pruned off the end
133 |
134 | ; Called on startup.
135 | __New(){
136 | static RIDEV_INPUTSINK := 0x00000100
137 |
138 | ; Create GUI (GUI needed to receive messages)
139 | ;Gui, Show, w100 h100
140 |
141 | ; Set TimeOutRate to negative value to have timer only fire once.
142 | this.TimeOutRate := this.TimeOutRate * -1
143 |
144 | ; Register mouse for WM_INPUT messages.
145 | DevSize := 8 + A_PtrSize
146 | VarSetCapacity(RAWINPUTDEVICE, DevSize)
147 | NumPut(1, RAWINPUTDEVICE, 0, "UShort")
148 | NumPut(2, RAWINPUTDEVICE, 2, "UShort")
149 | Flags := RIDEV_INPUTSINK
150 | NumPut(Flags, RAWINPUTDEVICE, 4, "Uint")
151 | NumPut(WinExist("A"), RAWINPUTDEVICE, 8, "Uint")
152 | r := DllCall("user32.dll\RegisterRawInputDevices", "Ptr", &RAWINPUTDEVICE, "UInt", 1, "UInt", DevSize )
153 |
154 | fn := this.MouseMoved.Bind(this)
155 | this.MoveFunc := fn
156 | this.ListenForMouseMovement(1)
157 |
158 | ; Initialize
159 | this.TimeOutFunc := this.DoRoll.Bind(this)
160 | this.InitHistory()
161 | }
162 |
163 | ; Turns on or off listening for mouse movement
164 | ListenForMouseMovement(mode){
165 | fn := this.MoveFunc
166 | if (mode){
167 | OnMessage(0x00FF, fn)
168 | } else {
169 | OnMessage(0x00FF, fn, 0)
170 | }
171 | }
172 |
173 | ; Called when the mouse moved.
174 | ; Messages tend to contain small (+/- 1) movements, and happen frequently (~20ms)
175 | MouseMoved(wParam, lParam, code){
176 | static MAX_TIME := 1000000 ; Only cache values for this long.
177 |
178 | ; RawInput statics
179 | static DeviceSize := 2 * A_PtrSize, iSize := 0, sz := 0, offsets := {x: (20+A_PtrSize*2), y: (24+A_PtrSize*2), button: (18+A_PtrSize*2)}, uRawInput
180 |
181 | static axes := {x: 1, y: 2}
182 |
183 | Critical
184 | VarSetCapacity(raw, 40, 0)
185 | If (!DllCall("GetRawInputData",uint,lParam,uint,0x10000003,uint,&raw,"uint*",40,uint, 16) || ErrorLevel || !NumGet(raw, 8))
186 | Return 0 ; Ignore events with a Device ID of 0 - these are mouse movements we sent using mouse_event
187 | ; Find size of rawinput data - only needs to be run the first time.
188 | if (!iSize){
189 | r := DllCall("user32.dll\GetRawInputData", "Ptr", lParam, "UInt", 0x10000003, "Ptr", 0, "UInt*", iSize, "UInt", 8 + (A_PtrSize * 2))
190 | VarSetCapacity(uRawInput, iSize)
191 | }
192 | sz := iSize ; param gets overwritten with # of bytes output, so preserve iSize
193 | ; Get RawInput data
194 | r := DllCall("user32.dll\GetRawInputData", "Ptr", lParam, "UInt", 0x10000003, "Ptr", &uRawInput, "UInt*", sz, "UInt", 8 + (A_PtrSize * 2))
195 |
196 | ; ignore button activity
197 | if (NumGet(&uRawInput, offsets.button, "Int") == 0){
198 | return
199 | }
200 |
201 | moved := {x: 0, y: 0}
202 |
203 | for axis in axes {
204 | obj := {}
205 | obj.delta_move := NumGet(&uRawInput, offsets[axis], "Int")
206 | obj.abs_delta_move := abs(obj.delta_move)
207 | obj.sgn_move := (obj.abs_delta_move = obj.delta_move) ? 1 : -1
208 |
209 | if (obj.abs_delta_move >= this.MoveThreshold[axis]){
210 | moved[axis] := 1
211 | }
212 |
213 | this.UpdateHistory(axis, obj)
214 | }
215 |
216 | if (moved.x || moved.y){
217 | ; A move over the threshold was detected.
218 | this.ChangeState(this.STATE_OVER_THRESH)
219 | } else {
220 | this.ChangeState(this.STATE_UNDER_THRESH)
221 | }
222 |
223 | }
224 |
225 | UpdateHistory(axis, obj){
226 | this.History[axis].InsertAt(1, obj)
227 | ; Enforce max number of entries
228 | max := this.History[axis].Length()
229 | if (max > (this.MOVE_BUFFER_SIZE - 1)){
230 | this.History[axis].RemoveAt(max, max - this.MOVE_BUFFER_SIZE)
231 | }
232 | }
233 |
234 | ; A timeout occurred - Perform a roll
235 | DoRoll(){
236 | static axes := {x: 1, y: 2}
237 |
238 | ;s := ""
239 |
240 | if (this.State != this.STATE_ROLLING){
241 | ; If roll has just started, calculate roll vector from movement history
242 | this.LastMove := {x: 0, y: 0}
243 |
244 | for axis in axes {
245 | ;s .= axis ": "
246 | trend := 0
247 | if (this.History[axis].Length() < this.MOVE_BUFFER_SIZE){
248 | ; ignore gestures that are too short
249 | continue
250 | }
251 | Loop % this.History[axis].Length() {
252 | if (A_Index != 1){
253 | ; Calculate the trend of the history.
254 | trend += (this.History[axis][A_Index].delta_move - this.History[axis][A_Index-1].delta_move)
255 | }
256 | this.LastMove[axis] += this.History[axis][A_Index].delta_move
257 | s .= this.History[axis][A_Index].delta_move ","
258 | }
259 | ;s .= "(" trend ")`n"
260 | /*
261 | Disabled, as seems to break mouse trackpads.
262 | Also seems to stop MoveFactor being applied to both axes?
263 | if (sgn(trend) != sgn(this.History[axis][1].delta_move)){
264 | ; downward trend of move speed detected - this is probably a normal stop of the mouse, not a lift
265 | continue
266 | }
267 | */
268 | this.LastMove[axis] := round(this.LastMove[axis] * this.MoveFactor[axis])
269 | }
270 | }
271 |
272 | if (this.LastMove.x = 0 && this.LastMove.y = 0){
273 | return
274 | }
275 | this.ChangeState(this.STATE_ROLLING)
276 |
277 | ;OutputDebug % "ROLL DETECTED: `n" s "Rolling x: " this.LastMove.x ", y: " this.LastMove.y "`n`n"
278 | fn := this.MoveFunc
279 | while (this.State == this.STATE_ROLLING){
280 | ; Send output
281 | DllCall("user32.dll\mouse_event", "UInt", 0x0001, "UInt", this.LastMove.x, "UInt", this.LastMove.y, "UInt", 0, "UPtr", 0)
282 | if (this.Friction){
283 | this.LastMove.x := this.ApplyFriction(this.LastMove.x, this.Friction)
284 | this.LastMove.y := this.ApplyFriction(this.LastMove.y, this.Friction)
285 | if (this.LastMove.x == 0 && this.LastMove.y == 0){
286 | this.State := this.STATE_UNDER_THRESH
287 | break
288 | }
289 | }
290 | ; Wait for a bit (allow real mouse movement to be detected, which will turn off roll)
291 | Sleep % this.RollFreq
292 | }
293 |
294 | }
295 |
296 | ApplyFriction(value, Friction){
297 | if (value < 0){
298 | was_negative := true
299 | value *= -1
300 | }
301 | value -= Friction
302 | if (value < 0)
303 | value := 0
304 | if (was_negative)
305 | value *= -1
306 | return value
307 | }
308 |
309 | InitHistory(){
310 | this.History := {x: [], y: []}
311 | }
312 |
313 | ChangeState(newstate){
314 | fn := this.TimeOutFunc
315 | if (this.State != newstate){
316 | ;OutputDebug, % "Changing State to : " this.StateNames[newstate]
317 | this.State := newstate
318 | }
319 |
320 | ; DO NOT return if this.State == newstate!
321 | ; We need to reset the timer!
322 |
323 | if (this.State = this.STATE_UNDER_THRESH){
324 | ; Kill the timer
325 | SetTimer % fn, Off
326 | ; Clear the history
327 | this.InitHistory()
328 | } else if (this.State = this.STATE_OVER_THRESH){
329 | ; Mouse is moving fast - start timer to detect sudden stop in messages (mouse was lifted in motion)
330 | SetTimer % fn, % this.TimeOutRate
331 | }
332 | /* else if (this.State = this.STATE_ROLLING){
333 | ;this.LastMove := {x: 0, y: 0}
334 | }
335 | */
336 | }
337 | }
338 |
339 | Sgn(val){
340 | if (val > 0){
341 | return 1
342 | } else if (val < 0){
343 | return -1
344 | } else {
345 | return 0
346 | }
347 | }
348 |
349 | Quit:
350 | ExitApp
351 | return
352 |
353 | ; Minimze to tray by SKAN http://www.autohotkey.com/board/topic/32487-simple-minimize-to-tray/
354 | WM_SYSCOMMAND(wParam){
355 | If ( wParam = 61472 ) {
356 | SetTimer, OnMinimizeButton, -1
357 | Return 0
358 | }
359 | }
360 |
361 |
362 | Menu( MenuName, Cmd, P3="", P4="", P5="" ) {
363 | Menu, %MenuName%, %Cmd%, %P3%, %P4%, %P5%
364 | Return errorLevel
365 | }
366 |
367 | OnMinimizeButton:
368 | MinimizeGuiToTray( R, Gui1 )
369 | Menu("Tray","Icon")
370 | Return
371 |
372 | GuiShow:
373 | DllCall("DrawAnimatedRects", UInt,Gui1, Int,3, UInt,&R+16, UInt,&R )
374 | Menu("Tray","NoIcon")
375 | Gui, Show
376 | Return
377 |
378 | MinimizeGuiToTray( ByRef R, hGui ) {
379 | WinGetPos, X0,Y0,W0,H0, % "ahk_id " (Tray:=WinExist("ahk_class Shell_TrayWnd"))
380 | ControlGetPos, X1,Y1,W1,H1, TrayNotifyWnd1,ahk_id %Tray%
381 | SW:=A_ScreenWidth,SH:=A_ScreenHeight,X:=SW-W1,Y:=SH-H1,P:=((Y0>(SH/3))?("B"):(X0>(SW/3))
382 | ? ("R"):((X0<(SW/3))&&(H0<(SH/3)))?("T"):("L")),((P="L")?(X:=X1+W0):(P="T")?(Y:=Y1+H0):)
383 | VarSetCapacity(R,32,0), DllCall( "GetWindowRect",UInt,hGui,UInt,&R)
384 | NumPut(X,R,16), NumPut(Y,R,20), DllCall("RtlMoveMemory",UInt,&R+24,UInt,&R+16,UInt,8 )
385 | DllCall("DrawAnimatedRects", UInt,hGui, Int,3, UInt,&R, UInt,&R+16 )
386 | WinHide, ahk_id %hGui%
387 | }
388 |
389 | #Include
--------------------------------------------------------------------------------
/rollmouse.au.txt:
--------------------------------------------------------------------------------
1 | version=1.0.9
--------------------------------------------------------------------------------
/rollmouse.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evilC/RollMouse/1db0e7097b73a602848a0daf10ee92ed5130ad87/rollmouse.exe
--------------------------------------------------------------------------------
/rollmouse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evilC/RollMouse/1db0e7097b73a602848a0daf10ee92ed5130ad87/rollmouse.png
--------------------------------------------------------------------------------