Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

Lex' Mouse Gestures


  • Please log in to reply
131 replies to this topic
pajenn
  • Members
  • 391 posts
  • Last active: Feb 06 2015 07:57 AM
  • Joined: 07 Feb 2009
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.

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.

Hardware: fast laptop with SSD
Software: Win 7 Home Premium 64-bit, android for phone and tablet


Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006
I suggest moving the code in Gestures_Down to some point below the following line:
Hotkey, *%m_LastGestureKey% Up, GestureKey_Up, On
There are at least two conditions that can prevent the script from reaching this line, and therefore prevent the hotkey from being (re)enabled. If the hotkey is not enabled, the trails won't be disabled when you release the key/button.

Please note:

The GetDC function retrieves a common, class, or private DC depending on the class style of the specified window. ... After painting with a common DC, the ReleaseDC function must be called to release the DC. Class and private DCs do not have to be released.
Source: MSDN: GetDC Function

ReleaseDC should be called from GestureKey_Up.

substituted in a transparent splashscreen for the background

SplashImage always produces an opaque window; I guess you see an effect similar to transparency because the transparent GIF prevents the window from painting its background over the top of whatever was on-screen before the window appeared. This "technique" won't work with desktop composition (Aero) enabled on Windows 7 (or probably Vista). There are two alternatives that I know of:
[*:206qmmcv]Use Gui, Color and WinSet, TransColor to make any region you have not painted over transparent.
[*:206qmmcv]Copy the screen into a bitmap via GDI before showing the Gui. This method would be faster (allowing gestures to be more responsive) but probably more complicated, and would hide any changes which are occurring to windows in the background.Note that SetPixel is very slow, even without the overhead of AutoHotkey (script). I instead use MoveToEx and LineTo. To define the colour and width of the line, CreatePen and SelectObject must be used.

Rather than trying to explain how to modify the script, I've updated the download with trail support. Define the following variables in Gestures.ini or the script itself:
m_PenWidth = 6
m_PenColor = 0000FF
The key parts are under if hdc_canvas in GestureKey_Up/Down and under if m_PenWidth in GestureKey_Down.

pajenn
  • Members
  • 391 posts
  • Last active: Feb 06 2015 07:57 AM
  • Joined: 07 Feb 2009
Thank you for adding trail support. The comments in the code are very helpful too.

Hardware: fast laptop with SSD
Software: Win 7 Home Premium 64-bit, android for phone and tablet


pajenn
  • Members
  • 391 posts
  • Last active: Feb 06 2015 07:57 AM
  • Joined: 07 Feb 2009
Hello Lexikos. I took a closer look at the script a few minutes ago, and I'm wondering if there's a typo of sorts (variable mix-up), though more likely I'm just missing something. The thing is I don't see why the script doesn't use "totalDistance" (instead of distance) for calculating angles/strokes. Here's a bit of the code from the first post (starting little below line 300), with my comments in caps (and red which may not have come thru properly):

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 )
        {
            if hdc_canvas
                ; Draw a line to the current mouse position, from the starting position or end of the previous line.
                DllCall( "LineTo", "uint", hdc_canvas, "int", x, "int", y )
            
            ; calculate distance and angle from previous position
            distance := G_GetLength(offsetX, offsetY)
[color=red];WHY NOT USE 	totalDistance += distance
;AND			if ( totalDistance > m_LowThreshold )
;INSTEAD OF LINE BELOW[/color]
            if ( distance > m_LowThreshold )
            {
[color=red];ANY PROBLEM IF I ADD 		if ( totalDistance > m_lastStrokeDistance/2 )
;WHERE m_lastStrokeDistance would be the last stroke's total length (0 for m_StrokeCount==0)[/color]
                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
                }
[color=red];I WOULD NEED TO REMOVE THE ELSE STATEMENT BELOW[/color]
				else
                {
                    totalDistance += distance
                }[color=red]
;BUT m_HighThreshold = 0 BY DEFAULT, SO CONDITION BELOW IS NEVER USED AND HENCE TOTAL DISTANCE IS NEVER USED?[/color]
                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
                }
            }
        }

On an unrelated note, I've enjoyed the script a lot. The missed mouse click issues went away with the introduction of the trails, though on rare occasions I think the transparent GUI fails to unload and because nothing responds to left-clicks until I right-click again - but it's not a problem...

Hardware: fast laptop with SSD
Software: Win 7 Home Premium 64-bit, android for phone and tablet


Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006
Since lastX and lastY are only set inside the IF, if distance <= m_LowThreshold, the next iteration will use the same point of origin as the current iteration. Therefore totalDistance += distance would be incorrect. For instance, if on the first iteration the mouse is {0,5} from origin and on the second iteration the mouse is {0,15} from the same origin, totalDistance will be 20 where it should be 15. How fast the loop iterates would determine how fast totalDistance accumulates. In short, it is correct the way it is.

;ANY PROBLEM IF I ADD if ( totalDistance > m_lastStrokeDistance/2 )
;WHERE m_lastStrokeDistance would be the last stroke's total length (0 for m_StrokeCount==0)

It should not be a problem (if you use distance instead of totalDistance), but you must make sure to maintain m_lastStrokeDistance. Specifically...

;I WOULD NEED TO REMOVE THE ELSE STATEMENT BELOW

...I suppose you should do something like m_lastStrokeDistance += distance in that ELSE, otherwise it will usually be just slightly greater than m_LowThreshold.

;BUT m_HighThreshold = 0 BY DEFAULT, SO CONDITION BELOW IS NEVER USED AND HENCE TOTAL DISTANCE IS NEVER USED?

Correct. As with any other global variable, m_HighThreshold can be set in Gestures.ini or one of the script files.

pajenn
  • Members
  • 391 posts
  • Last active: Feb 06 2015 07:57 AM
  • Joined: 07 Feb 2009

Since lastX and lastY are only set inside the IF, if distance <= m_LowThreshold, the next iteration will use the same point of origin as the current iteration. Therefore totalDistance += distance would be incorrect.


Thanks for taking the time to clear that up for me.

;ANY PROBLEM IF I ADD if ( totalDistance > m_lastStrokeDistance/2 )
;WHERE m_lastStrokeDistance would be the last stroke's total length (0 for m_StrokeCount==0)

It should not be a problem (if you use distance instead of totalDistance), but you must make sure to maintain m_lastStrokeDistance.


I added "if ( (total)distance > m_lastStrokeDistance/2 )" to require strokes to be about as long as their respective predecessors, and elsewhere I added "if ( (total)distance > m_lastStrokeDistance*2 ) , [then count it as two consecutive strokes in the same direction]." I simply wanted to see how it would handle.

I also tried the 'm_InitialZoneCount = 8' option with 'm_ZoneCount = 4', but added 'if ( m_InitialZoneCount = 8 ) [then//] lastZone *= .5' so that, for example, L = 4 with 8 zones, matches L = 2 with 4 zones for purposes of the condition 'if ( lastZone != zone )' (~line 347 of posted version of gestures.ahk). I wanted them to match so that my relative restrictions would work properly, but I wasn't sure if the 'm_InitialZoneCount = 8' option was intended to also allow consecutive (initial) strokes in directions L,D,U so I thought I'd mention it.

Again, great script with lots of flexibility to customize (although I will probably revert to its most basic format after I've played around with it enough).

Hardware: fast laptop with SSD
Software: Win 7 Home Premium 64-bit, android for phone and tablet


Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

I wasn't sure if the 'm_InitialZoneCount = 8' option was intended to also allow consecutive (initial) strokes in directions L,D,U

That was overlooked. I've updated the script with two notable changes:
[*:1gzrd453]Continuing mouse movement within the initial zone will extend the initial stroke.
[*:1gzrd453]Zones are resolved to text early (by G_GetZone) rather than when the gesture key is released.Your '*= .5' idea is clever, but doesn't seem to work well when the initial stroke is diagonal.

pajenn
  • Members
  • 391 posts
  • Last active: Feb 06 2015 07:57 AM
  • Joined: 07 Feb 2009

I've updated the script with two notable changes: [*:1mwvu0b5]Continuing mouse movement within the initial zone will extend the initial stroke.
[*:1mwvu0b5]Zones are resolved to text early (by G_GetZone) rather than when the gesture key is released.Your '*= .5' idea is clever, but doesn't seem to work well when the initial stroke is diagonal.


I was using the '*= .5' idea only for the first stroke, and right before "if ( lastZone != zone )", so diagonal strokes became 0.5, 1.5, 2.5 or 3.5, and appropriately did not equal any 4 zone numbers. Since zone is used for the pseudo array and to update lastZone if the if-statement is satisfied, the '*=.5' didn't carry over beyond the if-statement.

fwiw, below is how i applied it and the 'lastDistance' conditions (I set lastDistance:= 2*m_LowThreshold at the start of GestureKey_Down label).

if ( distance > m_LowThreshold && distance > lastDistance/2 )
            {
                angle:= G_GetAngle(offsetX, offsetY), angle*= c_Degrees, lastX:= x, lastY:= y
                if ( m_StrokeCount > 0 || m_InitialZoneCount < 2 )  ; get zone of angle
                    zone:= G_GetZone(angle, m_ZoneCount)
                else zone:= G_GetZone(angle, m_InitialZoneCount)
                
                if (m_StrokeCount = 1 && m_InitialZoneCount = 8)
                    lastZone *= .5
                
                if ( lastZone != zone ) || (lastZone = zone && distance > 2*lastDistance)
                    lastDistance:= distance, totalDistance:= distance, ++m_StrokeCount, m_Stroke%m_StrokeCount%:= zone, lastZone:= zone, waitCounter:= 0
                else totalDistance+= distance

Hardware: fast laptop with SSD
Software: Win 7 Home Premium 64-bit, android for phone and tablet


Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

I was using the '*= .5' idea only for the first stroke, and right before "if ( lastZone != zone )", so diagonal strokes became 0.5, 1.5, 2.5 or 3.5, and appropriately did not equal any 4 zone numbers.

However, a diagonal stroke can't be continued past 2*m_LowThreshold since the moment it passes m_LowThreshold, zone count reduces to 4 and diagonals are no longer recognized.

(I set lastDistance:= 2*m_LowThreshold at the start of GestureKey_Down label).
...
if ( distance > m_LowThreshold && distance > lastDistance/2 )

In that case, is distance > m_LowThreshold not redundant?

angle:= G_GetAngle(offsetX, offsetY), angle*= c_Degrees

If you're going to compact the expressions, the above can be written as:
angle:= G_GetAngle(offsetX, offsetY) * c_Degrees
Also, the bit in red can be removed:
lastDistance:= [color=red]distance,[/color] totalDistance:= distance
Why not simply use totalDistance? Actually, it looks like lastDistance is only the first part of the stroke (arbitrary since it depends on how fast the script runs), whereas totalDistance is at any moment the actual length of the last stroke.

pajenn
  • Members
  • 391 posts
  • Last active: Feb 06 2015 07:57 AM
  • Joined: 07 Feb 2009

I was using the '*= .5' idea only for the first stroke, and right before "if ( lastZone != zone )", so diagonal strokes became 0.5, 1.5, 2.5 or 3.5, and appropriately did not equal any 4 zone numbers.

However, a diagonal stroke can't be continued past 2*m_LowThreshold since the moment it passes m_LowThreshold, zone count reduces to 4 and diagonals are no longer recognized.


I don't see the issue. If you are using 'm_InitialZoneCount = 8' (with 'm_ZoneCount = 4') instead of 'm_ZoneCount = 8' altogether, presumably you would not want to continue diagonals beyond the first stroke.

(I set lastDistance:= 2*m_LowThreshold at the start of GestureKey_Down label).
...
if ( distance > m_LowThreshold && distance > lastDistance/2 )

In that case, is distance > m_LowThreshold not redundant?


It's just for the first stroke. Also, I set m_LowThreshold = 80 (up from default of 10), because all my intended strokes are longer than 80 anyway, so a lower threshold would just have picked up noise and inadvertent fluctuations in my attempts at straight lines.

So if the first stroke is 90, then for the next stroke lastDistance/2 = 45, and m_LowThreshold = 80 would become the effective lower bound.

angle:= G_GetAngle(offsetX, offsetY), angle*= c_Degrees

If you're going to compact the expressions, the above can be written as:
angle:= G_GetAngle(offsetX, offsetY) * c_Degrees


Thanks. Not sure if compacting does anything, but I read in the help file that

In v1.0.48+, the comma operator is usually faster than writing separate expressions, especially when assigning one variable to another (e.g. x:=y, a:=B). Performance continues to improve as more and more expressions are combined into a single expression; for example, it may be 35% faster to combine five or ten simple expressions into a single expression.

so I now I try to write everything possible in one expression or line.

Also, the bit in red can be removed:

lastDistance:= [color=red]distance,[/color] totalDistance:= distance
Why not simply use totalDistance?


But totalDistance is updated by the subsequent else statement 'if ( lastZone != zone )' fails, whereas the m_StrokeCount is not, then it would not equal the last stroke distance anymore.

Actually, it looks like lastDistance is only the first part of the stroke (arbitrary since it depends on how fast the script runs), whereas totalDistance is at any moment the actual length of the last stroke.


It's different if that else-statement below 'if ( lastZone != zone )' is ever reached, but now I'm not sure if can be...

Hardware: fast laptop with SSD
Software: Win 7 Home Premium 64-bit, android for phone and tablet


Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006
I'm not sure you understand fully how the script works, so I'll try to explain a few things:

A stroke begins when the mouse begins moving or changes directions, and ends only when the button is released or the mouse changes directions again. If the mouse stops moving but resumes movement in the same direction before the button is released, it is considered an extension of the same stroke. Since the loop iterates at an arbitrary speed^ and the speed of mouse movement varies, a single stroke may correspond to a single iteration or it may span many iterations.

In the first iteration of any given stroke, lastDistance and totalDistance are set to the distance of that first part of the stroke. If the mouse continues moving in the same direction, totalDistance is updated to contain the new length of the continued stroke. At the point immediately before the next stroke begins (where you are using lastDistance in the IF), lastDistance may be set to any value between m_LowThreshold and totalDistance, depending on how quickly the loop is iterating and the speed of the mouse.

^ In theory, each iteration should take at least m_Interval milliseconds. In practice, Sleep 50 doesn't necessarily sleep 50 milliseconds - on my system an iteration takes 46 milliseconds on average.

... presumably you would not want to continue diagonals beyond the first stroke.

I don't want to continue diagonals beyond the first stroke; I want the first stroke to continue beyond 20 pixels (or in your case 160 pixels).

Not sure if compacting does anything,

It is probably not worth doing in this case since only a miniscule amount of time is spent outside of Sleep. In my short tests, "a miniscule amount" meant < 15µs (0.015ms); even the shortest sleep is around 15ms on most systems.

But totalDistance is updated by the subsequent else statement 'if ( lastZone != zone )' fails, whereas the m_StrokeCount is not, then it would not equal the last stroke distance anymore.

I guess by "m_StrokeCount" you meant "lastDistance"? (Otherwise I don't see the relevance.) The else below if ( lastZone != zone ) updates totalDistance when a stroke is extended, not when a new stroke is detected. totalDistance seems more appropriate to me because it is updated.

It's different if that else-statement below 'if ( lastZone != zone )' is ever reached, but now I'm not sure if can be...

What were you referring to when you said "the subsequent else statement 'if ( lastZone != zone )' fails" if not the else below if ( lastZone != zone )?

pajenn
  • Members
  • 391 posts
  • Last active: Feb 06 2015 07:57 AM
  • Joined: 07 Feb 2009
Ok, I see how it works now. I got confused about the sequence of events - I began to think strokes were recorded when they ended, as opposed to when they start. I could elaborate, but I think it's simpler if I just ask people to ignore my last post. (TO PEOPLE: Please ignore my last post!)

Hardware: fast laptop with SSD
Software: Win 7 Home Premium 64-bit, android for phone and tablet


anjan_oleti
  • Members
  • 12 posts
  • Last active: Apr 12 2016 01:05 AM
  • Joined: 29 May 2009
I need to make a gesture without holding any of the mouse buttons... but, the script requires me to hold a mouse button (right, by default)

Is that possible? Would be of great help... Thanks!

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006
It would be an interesting feature, but mightn't be easy to implement. Currently the start and end of a gesture are clearly defined by button-press and release. Without anything clearly defining the beginning and end of the gesture, the script would need to continuously compare the history of strokes to a list of defined gestures (which currently does not exist). For instance, a movement {left, down, right} may be interpreted as L, L_D_R, R, etc.

Perhaps the best solution would be to require the mouse to remain stationary for a configurable amount of time at the beginning and end of a gesture. In contrast to m_Timeout, which applies only after the button is released, this timeout would need to be implemented inside the loop that watches mouse movement in order to end rather than cancel the gesture.

I'll think more about it, and perhaps see if I can implement it.

anjan_oleti
  • Members
  • 12 posts
  • Last active: Apr 12 2016 01:05 AM
  • Joined: 29 May 2009

It would be an interesting feature, but mightn't be easy to implement. Currently the start and end of a gesture are clearly defined by button-press and release. Without anything clearly defining the beginning and end of the gesture, the script would need to continuously compare the history of strokes to a list of defined gestures (which currently does not exist). For instance, a movement {left, down, right} may be interpreted as L, L_D_R, R, etc.

Perhaps the best solution would be to require the mouse to remain stationary for a configurable amount of time at the beginning and end of a gesture. In contrast to m_Timeout, which applies only after the button is released, this timeout would need to be implemented inside the loop that watches mouse movement in order to end rather than cancel the gesture.

I'll think more about it, and perhaps see if I can implement it.


Thanks for being open to the request!

Actually, I had a mouse "SHAKE" gesture in mind...
i.e. the mouse pointer moves in a zig-zag manner, 2 or 3 times, in a short time
yeah... like in the aero-shake feature in Windows 7, but without any button being held down.
Since the mouse is usually not moved in that manner, I thought it was something possible... I din't know that it was complicated, until I read your post ;)