1. I added OnMessage(0x200,"WM_MOUSEMOVE") next to your OnMessage(0x111, "WM_COMMAND") code, and the WM_MOUSEMOVE function is below your WM_COMMAND function.
WM_MOUSEMOVE(p_w,p_l) { global mode_draw, hdc_canvas, x_last, y_last x := p_l & 0xFFFF y := p_l >> 16 if mode_draw drawLine(x_last, y_last, x, y) x_last := x y_last := y }
2. To the GestureKey_Up label, I added the following 2 lines (at the top) to end draw mode and turn off the transparent splashimage covering:
mode_draw := False SplashImage, Off
3. To the GestureKey_Down label I added 4 lines (at the top) to load a transparent gif splashimage screen cover and turn on draw mode:
SplashImage, %A_ScriptDir%\canvas.gif,M b ,,,AutoHotkeyMousePath WinGet, hw_canvas, ID, AutoHotkeyMousePath hdc_canvas := DllCall( "GetDC", "uint", hw_canvas ) mode_draw:= True
4. At the end, I added a drawLine() function to trace the mouse path. The transparent gif splashimage is used to draw on it:
;Bresenham algorithm ;From: http://www.cs.unc.edu/~mcmillan/comp136/Lecture6/Lines.html drawLine(x0, y0, x1, y1) { global hdc_canvas dx:= x1-x0, dy:= y1-y0, stepx:= 0, stepy:= 0 if (dx < 0) { dx:= -dx, stepx:= -1 } else stepx:= 1 if (dy < 0) { dy:= -dy, stepy:= -1 } else stepy := 1 Loop 6 { x:= x0+A_Index-1, y:= y0 DllCall( "SetPixel", "uint", hdc_canvas, "int", x, "int", y, "uint", 0xFF0000 ) } if (dx > dy) { fraction:= dy-(dx >> 1) Loop { if (x0 = x1) break if (fraction >= 0) { y0+= stepy, fraction-= dx } x0+= stepx, fraction+= dy Loop 6 { x:= x0+A_Index-1, y:= y0 DllCall( "SetPixel", "uint", hdc_canvas, "int", x, "int", y, "uint", 0xFF0000 ) } } } else { fraction:= dx-(dy >> 1) Loop { if (y0 = y1) break if (fraction >= 0) { x0+= stepx, fraction-= dy } y0+= stepy, fraction+= dx Loop 6 { x:= x0+A_Index-1, y:= y0 DllCall( "SetPixel", "uint", hdc_canvas, "int", x, "int", y, "uint", 0xFF0000 ) } } } }
5. I also added a transparent gif-image the size of my screen to the script directory. (Any graphics software can create one - I used Irfanview).
So far it seem to work fine, I checked that my gestures worked to launch 'My Computer' (App1), min/max/close windows, Firefox launches, ... But if you see a reason for me to put the extra lines of code further down (within GestureKey_Down especially), please let me know.
Here's the modified code of Gestures.ahk, though it has several other changes too (I added SciTE and Notepad2 as editors, and disabled the mouse wheel and xbuttons):
; ; AutoHotkey Version: 1.0.46.09 + ; Language: English ; Platform: Win XP, 2003, Vista, and probably 2000 ; Author: Steve Gray, aka Lexikos ; ; Script Function: ; Mouse gestures. ; - Allows abitrary number of directions (zones). ; - Allows any number of movements/strokes in a sequence. ; - Custom script (label) execution OR variable-based key-stroke simulation. ; - Variable-based key-strokes and options can be set via Gestures.ini ; (setting a variable-based gesture overrides any associated labels/custom scripts) ; gosub Gestures_Default_Init ; Read gesture definitions from Gestures.ini G_LoadGestures( A_ScriptDir . "\Gestures.ini" ) c_PI := 3.141592653589793 c_halfPI := 1.5707963267948965 ; Pi/2 c_Degrees := 57.29578 ; 180/Pi (degrees per radian) #NoEnv SendMode Input #SingleInstance force #KeyHistory 20 CoordMode, Mouse, Screen SetTitleMatchMode, 2 OnMessage(0x200,"WM_MOUSEMOVE") ; custom tray icon (also called by ToggleGestureSuspend) G_SetTrayIcon(true) ; Hook "Suspend Hotkeys" messages to update the tray icon. ; Note: This has the odd side-effect of "disabling" the tray menu ; if the script is paused from the tray menu. OnMessage(0x111, "WM_COMMAND") Menu, TRAY, Tip, Mouse Gestures Menu, TRAY, Add Menu, TRAY, Add, Edit &Gestures, EditGestures Menu, TRAY, Add, Edit &Default Gestures, EditGestures2 Menu, TRAY, Add, Edit &this script, EditThisScript ; RAlt or %m_GestureKey%, whichever was used last m_LastGestureKey := m_GestureKey ; m_Stroke%i% will be set to the actual strokes (starting at m_Stroke1) m_StrokeCount = 0 m_WaitForRelease := false m_PassKeyUp := false m_ClosingWindow := 0 ; Use code similar to this to disable gestures on a per-application basis: ; GroupAdd, Blacklist, Firefox ahk_class MozillaUIWindowClass ; Hotkey, IfWinNotActive, ahk_group Blacklist ; register the hotkey Hotkey, %m_GestureKey%, GestureKey_Down Hotkey, #%m_GestureKey%, ToggleGestureSuspend ; extra key if ( m_GestureKey2 && m_GestureKey2 != m_GestureKey ) { Hotkey, %m_GestureKey2%, GestureKey_Down Hotkey, #%m_GestureKey2%, ToggleGestureSuspend } if !m_GestureKey2 m_GestureKey2 := m_GestureKey ; see also: GestureKey_Down for GetKeyState(...) GroupAdd, WinCloseGroup, ahk_class ConsoleWindowClass GroupAdd, WinCloseGroup, ahk_class AutoHotkey return Gestures_Default_Init: #Include %A_ScriptDir%\Gestures Default.ahk return EditGestures: Run, Z:\My Programs\AutoHotkey\SciTE\SciTE.exe "%A_ScriptDir%\Gestures.ini",, UseErrorLevel if ErrorLevel = ERROR Run, Z:\My Programs\Notepad2\Notepad2.exe "%A_ScriptDir%\Gestures.ini" return EditGestures2: Run, Z:\My Programs\AutoHotkey\SciTE\SciTE.exe "%A_ScriptDir%\Gestures Default.ahk",, UseErrorLevel if ErrorLevel = ERROR Run, Z:\My Programs\Notepad2\Notepad2.exe "%A_ScriptDir%\Gestures Default.ahk" return EditThisScript: Run, Z:\My Programs\AutoHotkey\SciTE\SciTE.exe %A_ScriptFullPath%,, UseErrorLevel if ErrorLevel = ERROR Run, Z:\My Programs\Notepad2\Notepad2.exe "%A_ScriptDir%\Gestures.ahk" return /* ; Wheel Gestures: Gesture key + Wheel sends keystrokes as defined by ; Gesture_WheelUp and Gesture_WheelDown in Gestures.ini. WheelUp:: WheelDown:: gesture := c_GesturePrefix ? c_GesturePrefix : "Gesture" if (m_WaitForRelease && %gesture%_%A_ThisHotkey%) { ; holding gesture button && this wheel gesture has a defined action m_ScrolledWheel := true m_ExitLoop := true Send % %gesture%_%A_ThisHotkey% } else Send {%A_ThisHotkey%} return */ /********** XBUTTON HOTKEYS - included in the script for (my) convenience. XButton2 & LButton::MinimizeActiveWindow() ;WinMinimize, A XButton2 & RButton:: ifWinActive, ahk_group WinCloseGroup WinClose, A else Send !{F4} return ; Task-switch with XButton1( + Wheel). XButton1 & WheelUp::AltTab XButton1 & WheelDown::ShiftAltTab XButton1::Send !{Tab} ; Document/tab-switch with XButton2( + Wheel) XButton2 & WheelUp::Send ^+{tab} XButton2 & WheelDown::Send ^{tab} XButton2::Send ^{Tab} */ MinimizeActiveWindow() { global lastMinTime := A_TickCount lastMinID := WinExist("A") ; unlike WinMinimize, using WM_SYSCOMMAND, SC_MINIMIZE ; causes the system-wide "Minimize" sound to be played PostMessage, 0x112, 0xF020 } ; Press Win + Gesture button to enable/disable gestures. ToggleGestureSuspend: Suspend, Toggle G_SetTrayIcon(!A_IsSuspended) ; wurt from: http://addons.miranda-im.org/details.php?action=viewfile&id=1512 if A_IsSuspended SoundPlay, %A_ScriptDir%\wurt_disabled.wav else SoundPlay, %A_ScriptDir%\wurt_enabled.wav return ; Press Escape to cancel the current gesture (before releasing the gesture button.) CancelGesture: Hotkey, Escape, CancelGesture, Off m_ExitLoop := true return GestureKey_Up: mode_draw := False SplashImage, Off m_WaitForRelease := false Hotkey, IfWinActive Hotkey, *%m_LastGestureKey% Up, GestureKey_Up, Off Hotkey, Escape, CancelGesture, Off ; record for later use MouseGetPos, m_EndX, m_EndY if ( m_PassKeyUp ) { Send {%m_LastGestureKey% Up} m_PassKeyUp := false } return GestureKey_Down: SplashImage, %A_ScriptDir%\canvas.gif,M b ,,,AutoHotkeyMousePath WinGet, hw_canvas, ID, AutoHotkeyMousePath hdc_canvas := DllCall( "GetDC", "uint", hw_canvas ) mode_draw:= True if ( !GetKeyState(m_GestureKey, "P") && !GetKeyState(m_GestureKey2, "P") ) return if ( m_WaitForRelease ) return m_WaitForRelease := true m_ExitLoop := false Hotkey, IfWinActive m_LastGestureKey := A_ThisHotkey Hotkey, *%m_LastGestureKey% Up, GestureKey_Up, On Hotkey, Escape, CancelGesture, On waitCounter := 0 startX := -1 startY := -1 totalDistance := 0 zone := 0 lastZone := -1 m_StrokeCount := 0 ; get starting mouse position MouseGetPos, lastX, lastY ; record for later use m_StartX := lastX m_StartY := lastY Loop { ; wait for mouse to move Sleep, m_Interval if ( m_ExitLoop ) { if m_ScrolledWheel KeyWait, %m_LastGestureKey% return } ; increment waitCounter by timer interval ; (may not be entirely accurate if the script is lagging...) waitCounter += m_Interval if ( !m_StrokeCount && m_InitialTimeout && waitCounter > m_InitialTimeout ) { if ( GetKeyState(m_LastGestureKey, "P") ) { if m_LastGestureKey in LButton,RButton ;,MButton { ; convert key name to a "button" (silly how MouseClick won't accept "LButton") StringLeft, btn, m_LastGestureKey, 1 ; remember position MouseGetPos, m_EndX, m_EndY ; move to point where gesture started, then click MouseClick, %btn%, m_StartX, m_StartY, , 0, D ; move back into place MouseMove, m_EndX, m_EndY, 0 btn = } else { Send {%m_LastGestureKey%} } ; pass GestureKey Up on to active window m_PassKeyUp := true } return } if ( !GetKeyState(m_GestureKey, "P") && !GetKeyState(m_GestureKey2, "P") ) { ; use location mouse was released at x := m_EndX y := m_EndY } else ; get current mouse position MouseGetPos, x, y offsetX := x - lastX offsetY := y - lastY ; check if mouse has moved if ( offsetX!=0 || offsetY!=0 ) { ; calculate distance and angle from previous position distance := G_GetLength(offsetX, offsetY) if ( distance > m_LowThreshold ) { angle := G_GetAngle(offsetX, offsetY) angle *= c_Degrees lastX := x lastY := y ; get zone of angle if ( m_StrokeCount > 0 || m_InitialZoneCount < 2 ) zone := G_GetZone(angle, m_ZoneCount) else zone := G_GetZone(angle, m_InitialZoneCount) if ( zone == -1 ) { ;DEBUG ;MsgBox, 64, DEBUG, Gesture stroke went off-course! (Exceeded zone tolerance `%%m_Tolerance%.), 5 SoundPlay, *-1 return } if ( lastZone != zone ) { totalDistance := distance ; add stroke zone to the pseudo-array ++m_StrokeCount m_Stroke%m_StrokeCount% := zone lastZone := zone ; reset timeout counter waitCounter := 0 } else { totalDistance += distance } if ( m_HighThreshold > 0 && totalDistance > m_HighThreshold ) { ;DEBUG ;MsgBox, 64, DEBUG, Gesture stroke exceeded maximum stroke length: %totalDistance% / %m_HighThreshold%., 5 SoundPlay, *-1 Sleep, 150 SoundPlay, *-1 return } } } ; end loop when gesture key is released if ( !GetKeyState(m_GestureKey, "P") && !GetKeyState(m_GestureKey2, "P") ) break } ; cancel gesture if the mouse was immobile for too long after the last gesture if ( m_Timeout > 0 && waitCounter > m_Timeout ) { ;DEBUG SoundPlay, *-1 ;MsgBox, 64, DEBUG, Gesture timeout- %waitCounter% / %m_Timeout%, 5 if ( m_DefaultOnTimeout ) { gesture = Gesture_Default if ( %gesture% ) SendEvent % %gesture% else if ( IsLabel(gesture) ) gosub %gesture% } return } gesture := c_GesturePrefix ? c_GesturePrefix : "Gesture" Loop %m_StrokeCount% { ; get the zone of this stroke zone := m_Stroke%A_Index% ; get descriptive label for zone, if possible if ( A_Index == 1 && m_InitialZoneCount >= 2 ) zoneText := c_Zone%m_InitialZoneCount%_%zone% else zoneText := c_Zone%m_ZoneCount%_%zone% gesture .= "_" if ( zoneText ) gesture .= zoneText else gesture .= zone } if ( m_StrokeCount == 0 ) gesture = Gesture_Default ; if gesture points to a variable, send its contents as keystrokes if ( %gesture% ) Send % %gesture% ; else if gesture has a label (e.g Gesture_D_R = down, then right), go to it else if ( IsLabel(gesture) ) gosub %gesture% else MsgBox, , , Unknown gesture with %m_StrokeCount% strokes:`n %gesture%, 2 return ; get angle of {x,y} from positive x-axis, relative to {0,0} ; return value is in RADIANS G_GetAngle( x, y ) { global c_PI, c_halfPI ; if {0,0}, angle can be any real value if ( x==0 && y==0 ) return 0 if ( x > 0 ) { if ( y >= 0 ) return ATan(y/x) ;y < 0 return ATan(y/x) + 2*c_PI } else if ( x < 0 ) { return ATan(y/x) + c_PI } else ; x == 0 { if ( y > 0 ) return c_halfPI ;y < 0 return -c_halfPI } } ; get distance of {x,y} from {0,0} G_GetLength( x, y ) { return Sqrt(x*x + y*y) } ; get the zone of an angle ; angle: specified in degrees ; zoneCount: number of zones (zone centers are at (360/zoneCount) degree intervals) G_GetZone( angle, zoneCount ) { local degPerZone local zone local tolerance if ( zoneCount < 2 ) { ; show debug error message MsgBox, 16, ERROR, Invalid zoneCount (%zoneCount%) passed to G_GetZone()., 5 return 0 } if ( angle < 0 ) { Loop { ; while ( angle < 0 ) would be nice... angle += 360 if ( angle >= 0 ) break } } ; calculate zone size degPerZone := 360 / zoneCount ; calculate zone - Round() finds the nearest zone (nearest integer) zone := Mod( Round(angle/degPerZone), zoneCount ) ; calculate maximum tolerance tolerance := degPerZone/2.0 ; calculate real tolerance from user-defined tolerance (percentage) if ( m_Tolerance < 100 ) tolerance *= Abs(m_Tolerance)/100.0 if ( zone == 0 && angle > 180 ) angle -= 360 ; return -1 if angle is not within tolerance of the nearest zone if ( Abs(angle-(zone*degPerZone)) >= tolerance ) zone := -1 return zone } G_LoadGestures( filename ) { ; declaring one or more local variables forces newly created variables to be global in scope local line, varname ; declare a local array (0=array length, 1=first item) local lineParts0 ; these must be explicitly declared local ; (as of AHK 1.0.46.10, StringSplit creates them as local variables either way ; because lineParts0 is local, but references to 'lineParts1' in script refer ; to GLOBAL variables...) local lineParts1, lineParts2 Loop, Read, %filename% { ; ignore comments StringLeft, line, A_LoopReadLine, 1 if ( line = ";" ) continue ; replace " = " with uncommon delimiter StringReplace, line, A_LoopReadLine, %A_Space%=%A_Space%, ; split the string into (hopefully) two parts: variable, keys to send StringSplit, lineParts, line, , %A_Space%%A_Tab% if ( lineParts0 == 2 ) { if ( StrLen(lineParts1) > 0 ) { ; allow underscores by removing them from comparison StringReplace, varname, lineParts1, _, , All ; check if variable name is alphanumeric if varname is alnum { ; convert the key = value string to a variable %lineParts1% := lineParts2 } } else { MsgBox empty variable name on line %A_Index% } } } } WM_COMMAND(wParam, lParam) { static IsPaused, IsSuspended Critical id := wParam & 0xFFFF if id in 65305,65404,65306,65403 { ; "Suspend Hotkeys" or "Pause Script" if id in 65306,65403 ; pause IsPaused := ! IsPaused else ; at this point, A_IsSuspended has not yet been toggled. IsSuspended := ! A_IsSuspended G_SetTrayIcon(!(IsPaused or IsSuspended)) } } WM_MOUSEMOVE(p_w,p_l) { global mode_draw, hdc_canvas, x_last, y_last x := p_l & 0xFFFF y := p_l >> 16 if mode_draw drawLine(x_last, y_last, x, y) x_last := x y_last := y } G_SetTrayIcon(is_enabled) { icon := is_enabled ? "gestures.ico" : "nogestures.ico" icon = %A_ScriptDir%\%icon% ; avoid an error message if the icon doesn't exist IfExist, %icon% Menu, TRAY, Icon, %icon%,, 1 } ;Bresenham algorithm ;From: http://www.cs.unc.edu/~mcmillan/comp136/Lecture6/Lines.html drawLine(x0, y0, x1, y1) { global hdc_canvas dx:= x1-x0, dy:= y1-y0, stepx:= 0, stepy:= 0 if (dx < 0) { dx:= -dx, stepx:= -1 } else stepx:= 1 if (dy < 0) { dy:= -dy, stepy:= -1 } else stepy := 1 Loop 6 { x:= x0+A_Index-1, y:= y0 DllCall( "SetPixel", "uint", hdc_canvas, "int", x, "int", y, "uint", 0xFF0000 ) } if (dx > dy) { fraction:= dy-(dx >> 1) Loop { if (x0 = x1) break if (fraction >= 0) { y0+= stepy, fraction-= dx } x0+= stepx, fraction+= dy Loop 6 { x:= x0+A_Index-1, y:= y0 DllCall( "SetPixel", "uint", hdc_canvas, "int", x, "int", y, "uint", 0xFF0000 ) } } } else { fraction:= dx-(dy >> 1) Loop { if (y0 = y1) break if (fraction >= 0) { x0+= stepx, fraction-= dy } y0+= stepy, fraction+= dx Loop 6 { x:= x0+A_Index-1, y:= y0 DllCall( "SetPixel", "uint", hdc_canvas, "int", x, "int", y, "uint", 0xFF0000 ) } } } }
Notes: The drawline() function is from Metaxal's script. Also, bits of other code are from Metaxal and Shimanov draw-on-screen: <!-- m -->http://www.autohotke.../topic7378.html<!-- m --> .
I basically just stripped down their scripts to the bare minimum, substituted in a transparent splashscreen for the background, and integrated it into the gesture script. (For me that was quite an achievement though). You can easily change the color and thickness of the line (I'm using 0xFF0000 (blue) and thickness 6).
P.S. I haven't learned to DllCall yet, so if someone sees a way to improve those or anything else, please let me know.