├── .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 | ![ScreenShot](https://github.com/evilC/RollMouse/blob/master/rollmouse.png?raw=true) 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 --------------------------------------------------------------------------------