I got the trails working; at least so far so good, not sure if later on they will cause an issue. How does this sound Lexikos:
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.
Code:
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:
Code:
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:
Code:
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:
Code:
;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):
Code:
;
; 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:
http://www.autohotkey.com/forum/topic7378.html .
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.