I have added a few features to the script, as well as documenting its inner workings in excruciating detail for everyone's benefit. Enjoy!
For best results, view in SciTE4AutoHotKey.
Changes:
*Code documented
*Mouse routines replaced
*Added custom system tray icon (just put 2 icons labelled "scrollwheel.ico" and "scrollwheeldisabled.ico" in the script's directory).
*Added option to enable and disable drag-and-scroll functionality via tray menu or double-right-clicking.
; DragToScroll.ahk
;
; Scroll any active window by clicking and dragging with
; the right mouse button. Should not interfere with normal
; right clicking.
;
;
; Changes:
; Jun.28.2010
; * First release
; * Adds simple scroll acceleration to mirror mouse move speed
;
; Jun.30.2010
; * General Cleanup, Efficiency improvements
; * Better scroll implemetations, full choice of method
; * Added scrolling by WM_MOUSEWHEEL
;
; Jul.1.2010
; * Updated options menu
; * Increased precision in WM_MOUSEWHEEL
; * Nonlinear acceleration
;
; Jul.09.2010
; * Adds separable settings for vertical and horizontal scroll methods
; * New defaults: WheelMessage for Vertical, ScrollMessage for Horizontal
; * Better compatibility for horizontal scrolling
;
; Aug.31.2010
; *Code documented
; *Mouse routines replaced
; *Added custom system tray icon (just put 2 icons labelled "scrollwheel.ico" and "scrollwheeldisabled.ico" in the script's directory).
; *Added option to enable and disable drag-and-scroll functionality via tray menu or double-right-clicking.
DoubleClickThreshold := DllCall("GetDoubleClickTime")
DragDelay := DoubleClickThreshold ;in ms
PollFrequency = 20 ; in ms
ConfineToTarget := false
; scroll method
; mWheelKey - Simulate actual mouse wheel movement
; mWheelMessagee - Send messages WM_MOUSEWHEEL to the target control
; mScrollmessage - Send messages WM_HSCROLL and WM_VSCROLL
mWheelKey := "WheelKey"
mWheelMessage := "WheelMessage"
mScrollMessage := "ScrollMessage"
;
; choose one of above
; WheelMessage & WheelKey are preferred; your results may vary
ScrollMethodX := mScrollMessage
ScrollMethodY := mWheelMessage
; Speed & acceleration
Threshold = 5 ; in pixels
MaxAcceleration := 10 ; in levels, roughtly 1-10 suggested, 0 to disable
SpeedX := 1.0 ; as a multiplication constant
SpeedY := 1.0 ; as a multiplication constant
;What the acceleration function does is to precompute values of the acceleration function at
;certain points along the function and store them into a 1D array for easier retrieval.
; acceleration function -- modify carefully!!
; default is a pretty shallow parabolic curve
Loop, %MaxAcceleration%
Accel%A_Index% := .006 * A_Index **2 + 1
; Constants
;--------------------------------
WM_HSCROLL = 0x114
WM_VSCROLL = 0x115
WM_MOUSEWHEEL = 0x20A
WM_HMOUSEWHEEL = 0x20E
WHEEL_DELTA = 50
SB_LINEDOWN = 1
SB_LINEUP = 0
SB_LINELEFT = 0
SB_LINERIGHT = 1
;DragStatus
DS_NEW = 0
DS_DRAGGING = 1
DS_HANDLED = 2
; Init
;--------------------------------
OldY := ""
OriginalX := ""
OriginalY := ""
DragStatus := DS_NEW
;Mouse Routine Init
;--------------------------------
TimeOfLastRButtonDown := 0
TimeOf2ndLastRButtonDown:= 0
TimeOfLastRButtonUp := 0
ScrollDisabled := 0
Gosub MenuInit
Return
; Hotkeys
;--------------------------------
;I have torn out and replaced the old mouseclick detection routines with a more robust model (i.e. it works for me, whereas the old one didn't).
;
;Explanation of the hotkey routines:
;The routines are run every time the mouse button is pressed and released.
;When pressed, we log the time that it was pressed at; when released, we also log the time it was released.
;We keep a memory of the last 2 mouse button presses that have occurred, in a buffer of sorts, to help us detect double-clicks.
;We only keep a memory of the last button release that has occurred, and not the last 2 releases, because that would be unnecessary.
RButton::
Critical
;This is to force the RButton Down thread to be attended to before the RButton Up thread, so that a click (press-release event) is properly registered as a click.
;If not, a rapid click could cause the Button Up event to be processed before the Button Down event, thanks to AHK's pseudo-multithreaded handling of hotkeys.
TimeOf2ndLastRButtonDown := TimeOfLastRButtonDown ;Move the time of the 2nd last button press event down a space to make room for the latest button press event.
;The stack has only 2 spaces; even older values are discarded.
TimeOfLastRButtonDown := A_TickCount ;Push the time for the latest button press onto the stop of the stack.
DragStatus := DS_NEW ;For compatibility with the original code.
If (ScrollDisabled != 1) ;These lines check if the user has disabled the drag-and-scroll function.
SetTimer, DragCatcher, % -1 * DragDelay ;If not, it turns on the hold/drag watchdog timer to detect drags.
Return
RButton Up::
Critical
TimeOfLastRButtonUp := A_TickCount ;Log the time of the latest button release event.
SetTimer, DragCatcher, Off ;Stop the hold/drag watchdog timer, because we're not dragging anymore (if we were to begin with).
GoSub, DClickTest ;Test if this was the 2nd click of a double-click.
DragStatus := DS_HANDLED ;For compatibility with the original code.
Return
; Implementation
;--------------------------------
;This entire subroutine is executed once the mouse button has been held for a certain threshold.
DragCatcher:
DragStatus := DS_HANDLED ;Set DragStatus to show that a drag has been registered and being handed over to the drag-and-scroll routines.
GoSub DragStart ;Start the drag-and-scroll routines.
Return
DClickTest:
;Here, we assume that if the mouse button was released, then it had to be pressed down to begin with (reasonable?).
;We then check the time of the 2nd last button press event. (So, to recap, the mouse went press-release-press-release, and we are checking the 2nd last press).
DClickCalc := TimeOfLastRButtonUp - TimeOf2ndLastRButtonDown
;If the time difference between the 2nd last button press event and the latest button release is less than the DoubleClickThreshold, then we treat it as a double-click.
If (DClickCalc <= DoubleClickThreshold)
{
;The functions to be performed upon a double-click go here. I've set it to toggle the drag-and-scroll function.
ScrollDisabled := !ScrollDisabled ;Toggle the enable status of the drag-and-scroll function.
GoSub mnuCheckEnable ;Update the tray icon and settings menu to reflect the status.
}
;If not, then it is treated as a single click, or the end of a hold.
;Note, however, that the first click of a double-click is always treated as a single click first, i.e. if you were to log the events it would be
; <Time of first click> -- (Single Click)
; <Time of 2nd click> -- (Double Click)
;because the time lag between the first click of the double-click and the previous single click would be longer than the DoubleClickThreshold.
;
;A solution to eliminate this would be to wait until the DoubleClickThreshold has passed, then process the event as a single click.
;For me, this solution introduces unacceptable lag, making single clicks take longer to register than double clicks, which is weird.
If (DragStatus = DS_NEW) ;If this was NOT the end of a hold,
GoSub DragSkip ;Pass the mouse click to the program.
Else
GoSub DragStop ;Otherwise, stop the scrolling routines
Return
;This just passes the mouse click.
DragSkip:
Click Right ;Gotta love this function; it works better than Send.
Return
DragStop:
DragStatus := DS_HANDLED ;Reset the DragStatus flag.
OldX := "" ;Reset all other parameters as well
OldY := ""
NewX := ""
NewY := ""
OriginalX := ""
OriginalY := ""
SetTimer, DragStart, Off ;Disable the timer that DragStart uses to trigger its main loop, stopping DragStart as a result
Return
DragStart:
SetTimer, DragStart, % -1 * Abs(PollFrequency) ;Trigger this subroutine to run again once the Polling period has elapsed.
If !StrLen( OriginalY ) ;If OriginalY contains a zero-length string (i.e. nothing stored in it, because the user has just started to drag)
{
MouseGetPos, OldX, OldY,, Hwnd, 3 ;Get the mouse cursor position, store it in OldX and OldY,
;store the HWND of the window the mouse is hovering over, for use in the "Constrain to Active Window" mode.
OriginalX := OldX ;Change OriginalX to match OldX, Change OriginalY to match OldY.
OriginalY := OldY ;These coordinates will be where the cursor returns to once the dragging ends.
}
Else ;If we're in the middle of a drag,
{
DragStatus := DS_DRAGGING ;Change DragStatus to show that we're dragging
MouseGetPos, NewX, NewY,, NewHwnd, 3 ;Get the most current mouse cursor position and the HWND of the window currently being hovered over.
if (Hwnd != NewHwnd && ConfineToTarget) ;If the previous HWND does not match this new HWND, the "Constrain" mode was on,
GoSub DragStop ;it's the end of the line. You have moved out of the active window. Stop the routines.
;Calculate/Scroll - X
DiffX := Abs( NewX - OldX ) ;Calculate the absolute difference in X values between the cursor's current and previous position.
If (DiffX > Threshold) ;If the difference is over the threshold needed to start scrolling,
{
GoSub HScroll ;start scrolling (horizontally)
OldX := NewX ;and update the mouse cursor coordinates
OldY := NewY
}
;Calculate/Scroll - Y
DiffY := Abs( NewY - OldY ) ;Do the same for the Y coordinates
If (DiffY > Threshold)
{
GoSub VScroll
OldX := NewX
OldY := NewY
}
}
Return
VScroll: ;This is the business end; it simulates mousewheel/cursor input to scroll the active window vertically.
yTicks := DiffY / Threshold ;How fast is the mouse cursor's Y-coordinate changing, in terms of Ticks?
yFactor := yTicks * SpeedY ;Multiply this by the contant SpeedY. This is the vertical scroll speed.
if (MaxAcceleration > 0) ;If the acceleration routine is not disabled (i.e not set to 0), run it.
{
;The acceleration routine looks up pre-computed values of the acceleration function,
;which are stored in a 1D array. It then retreives the value and multiplies yFactor by that amount.
;The end result is that as you move the mouse cursor, the scrolling speed accelerates based on your cursor's speed.
yAccelIndex := Ceil(yFactor) ;This sets the index variable for the array lookup.
;If the index's value exceeds the bounds of the array, then set it to point to the last cell in the array.
yAccelIndex := (yAccelIndex<=MaxAcceleration ? yAccelIndex : MaxAcceleration)
yAccel := Accel%yAccelIndex% ;Retreive the value of the cell.
yFactor *= yAccel ;Multiply yFactor by that value and store it back in yFactor.
;_DEBUG_ ToolTip, t%yTicks% -> i%yAccelIndex% a%yAccel% -> f%yFactor% <--This is for debugging purposes.
}
;_DEBUG_ ToolTip Y t%yTicks% -> f%yFactor% <--This is for debugging purposes.
if (ScrollMethodY = mWheelMessage) ;If we are handling scrolling by passing WheelMessages,
{
;Multiply the base wheel scrolling speed by yFactor,
;and adjust its direction depending on whether the user moved his mouse up or down
wparam := WHEEL_DELTA * (NewY < OldY ? -1 : 1) * yFactor
;Then format the message properly and send it to the window the mouse is hovering over.
PostMessage, WM_MOUSEWHEEL, (wparam<<16), (OriginalY<<16)|OriginalX,, Ahk_ID %Hwnd%
}
else if (ScrollMethodY = mWheelKey) ;If we are handling scrolling by sending WheelUp and WheelDown messages
{
;Check if we have to send WheelUp or WheelDown messages depending on whether the user moved his mouse up or down
wparam := NewY < OldY ? "{WheelDown}" : "{WheelUp}"
;And send as many WheelUp/WheelDown messages as needed to scroll at the desired speed
Loop, %yFactor%
Send, %wparam%
}
else if (ScrollMethodY = mScrollMessage) ;If we are handling scrolling by sending Scroll messages (which work in terms of scrolling by lines)
{
wparam := NewY < OldY ? SB_LINEDOWN : SB_LINEUP ;Check if we have to send LINEDOWN or LINEUP messages
Loop, %yFactor% ;and send as many as needed to scroll at the desired speed
PostMessage, WM_VSCROLL, wparam, 0,, Ahk_ID %Hwnd%
}
Return
HScroll: ;Same thing for Horizontal Scrolling
xTicks := DiffX / Threshold
xFactor := xTicks * SpeedX
;_DEBUG_ ToolTip Y t%xTicks% -> f%xFactor% <-- This is for debugging purposes
if (ScrollMethodX = mWheelMessage)
{
wparam := WHEEL_DELTA * (NewX < OldX ? -1 : 1) * xFactor
PostMessage, WM_HMOUSEWHEEL, wparam, 0,, Ahk_ID %Hwnd%
}
else if (ScrollMethodX = mWheelKey)
{
wparam := NewX < OldX ? "{WheelLeft}" : "{WheelRight}"
Loop, %xFactor%
Send, %wparam%
}
else if (ScrollMethodX = mScrollMessage)
{
wparam := NewX < OldX ? SB_LINERIGHT : SB_LINELEFT
Loop, %xFactor%
PostMessage, WM_HSCROLL, wparam, 0,, Ahk_ID %Hwnd%
}
Return
; Menu
;--------------------------------
MenuInit:
; Enable/Disable
Menu, mnuSettings, ADD, Enable, mnuEnable
GoSub mnuCheckEnable
; Confine
Menu, mnuSettings, ADD
Menu, mnuSettings, ADD, Confine to Target Window, mnuConfineWindow
GoSub mnuConfineWindowInit
; Method
Menu, mnuSettings, ADD
Menu, mnuSettings, ADD, Wheel Key, mnuMethodWheelKey
Menu, mnuSettings, ADD, Wheel Message, mnuMethodWheelMessage
Menu, mnuSettings, ADD, Scroll Message, mnuMethodScrollMessage
if (ScrollMethodY = mWheelKey)
GoSub mnuMethodWheelKey
else if (ScrollMethodY = mWheelMessage)
GoSub mnuMethodWheelMessage
else if (ScrollMethodY = mScrollMessage)
GoSub mnuMethodScrollMessage
;add the whole menu to tray
Menu, TRAY, ADD
Menu, TRAY, ADD, Settings, :mnuSettings
Return
; Menu Handlers
;--------------------------------
mnuMethodWheelKey:
ScrollMethodY := mWheelKey
Menu, mnuSettings, Uncheck, Wheel Message
Menu, mnuSettings, Uncheck, Scroll Message
Menu, mnuSettings, Check, Wheel Key
Return
mnuMethodWheelMessage:
ScrollMethodY := mWheelMessage
Menu, mnuSettings, Uncheck, Wheel Key
Menu, mnuSettings, Check, Wheel Message
Menu, mnuSettings, Uncheck, Scroll Message
Return
mnuMethodScrollMessage:
ScrollMethodY := mScrollMessage
Menu, mnuSettings, Uncheck, Wheel Key
Menu, mnuSettings, Uncheck, Wheel Message
Menu, mnuSettings, Check, Scroll Message
Return
mnuConfineWindow:
ConfineToTarget := !ConfineToTarget
mnuConfineWindowInit:
if (ConfineToTarget)
Menu, mnuSettings, Check, Confine to Target Window
else
Menu, mnuSettings, Uncheck, Confine to Target Window
Return
mnuEnable:
ScrollDisabled := !ScrollDisabled
mnuCheckEnable:
If (ScrollDisabled = 0) {
Menu, mnuSettings, Check, Enable
Menu, TRAY, icon, %A_ScriptDir%\scrollwheel.ico, 0
Menu, TRAY, tip, Scrolling Enabled
return
} else {
Menu, mnuSettings, Uncheck, Enable
Menu, TRAY, icon, %A_ScriptDir%\scrollwheeldisabled.ico, , 0
Menu, TRAY, tip, Scrolling Disabled
}
Return