AutoHotkey Homepage AutoHotkey Community
Let's help each other out
 
 FAQFAQ   SearchSearch   MemberlistMemberlist   RegisterRegister 
 ProfileProfile   Log in to check your private messagesLog in to check your private messages   Log inLog in 

Function CountPresses() - easily detect multiple presses

 
Post new topic   Reply to topic    AutoHotkey Community Forum Index -> Scripts & Functions
View previous topic :: View next topic  
Author Message
valenock



Joined: 14 Sep 2004
Posts: 16

PostPosted: Fri Feb 16, 2007 11:25 pm    Post subject: Function CountPresses() - easily detect multiple presses Reply with quote

The function below can help you easily detect multiple consecutive presses of a hotkey.
For example, you can run 3 different actions if "WIN + F1" hotkey was pressed once within 0.4 seconds, twice or three times in a row.

I use it quite often for functions like Mute / Unmute, Dial / HangUp, etc.
It's nice to run an on screen reminder (OSD) to inform what action was actually chosen.

Thanks to Chris and PhiLho who helped to debug it. Much of the code was stolen from Autohotkey help file.

CountPresses(param1, param2, param3) takes 3 params. Each must be a name of the label where you want to be GoSub'd in case hotkey is pressed once, twice or three times.

Code:

$#F1::
CountPresses("DownloadON", "DownloadOFF", "NoActionDefined")
return

DownloadON:
OSD("Download::ON")
return

DownloadOFF:
OSD("Download::OFF")
return

NoActionDefined:
OSD("WARNING !!! no action has been defined for this number of presses")
return


CountPresses(param1, param2, param3)
{
   static key_presses = 0
   
   static pressed1 = 0
   static pressed2 = 0
   static pressed3 = 0
   
   pressed1 = %param1%
   pressed2 = %param2%
   pressed3 = %param3%
   
   if key_presses > 0 ; SetTimer already started, so we log the keypress instead.
   {
      key_presses += 1
      return
   }
   ; Otherwise, this is the first press of a new series. Set count to 1 and start
   ; the timer:
   key_presses = 1
   SetTimer, WaitKeyPress, 400 ; Wait for more presses within a 400 millisecond window.
   return

   WaitKeyPress:
   SetTimer, WaitKeyPress, off
   
   if key_presses = 1 ; The key was pressed once.
   {
      if(pressed1 = "")
         MsgBox "CountPresses(): No action was assigned to this hotkey"
      else
      GoSub, %pressed1%
   }
   else if key_presses = 2 ; The key was pressed twice.
   {
      if(pressed2 = "")
         MsgBox "CountPresses(): No action was assigned to this hotkey"
      else
      GoSub, %pressed2%
   }
   else if key_presses = 3 ; The key was pressed twice.
   {
      if(pressed3 = "")
         MsgBox "CountPresses(): No action was assigned to this hotkey"
      else
         GoSub, %pressed3%
   }
   ; Regardless of which action above was triggered, reset the count to
   ; prepare for the next series of presses:
   key_presses = 0
   
   return
}
Back to top
View user's profile Send private message
Laszlo



Joined: 14 Feb 2005
Posts: 4012
Location: Pittsburgh

PostPosted: Fri Feb 16, 2007 11:55 pm    Post subject: Reply with quote

Nice idea to pack all these in a function.
Have you thought about using KeyWait instead of a timer? It might be simpler.
Back to top
View user's profile Send private message
jonny



Joined: 13 Nov 2004
Posts: 3004
Location: Minnesota

PostPosted: Sat Feb 17, 2007 1:34 am    Post subject: Reply with quote

I don't think it would be simpler, because you'd need more involved code to track the 400-millisecond range when it doesn't time out.
Back to top
View user's profile Send private message
Laszlo



Joined: 14 Feb 2005
Posts: 4012
Location: Pittsburgh

PostPosted: Sat Feb 17, 2007 4:00 am    Post subject: Reply with quote

Code:
$#F1 UP::CountPresses("M1","M2","M3")
M1:
   MsgBox 1
Return
M2:
   MsgBox 2
Return
M3:
   MsgBox 3
Return

CountPresses(Label1, Label2, Label3) {
   Static Count
   key := RegExReplace(A_ThisHotKey,"[\*\~\$\#\+\!\^( UP)]")
   If ( A_ThisHotKey = A_PriorHotKey and A_TimeSincePriorHotkey < 400 )
        Count += Count < 3 ? 1 : 0
   Else Count = 1
   KeyWait %key%, DT0.4
   If (ErrorLevel)
      GoSub % Label%Count%
}
Back to top
View user's profile Send private message
Laszlo



Joined: 14 Feb 2005
Posts: 4012
Location: Pittsburgh

PostPosted: Sun Feb 18, 2007 12:18 am    Post subject: Reply with quote

The timer-less version of valenock's script works best, if the hotkey is activated when it is released (UP).

Sometimes you want the original function of the hotkey at single press, like F1 can call up normally the help of the current application, but double pressed F1 could invoke the AHK help. Below the subroutine KEY0 computes the key part of the current hotkey and sends it to the foreground application. It can be reused for many CountPresses calls.

Returning the press count can be useful, e.g. a subroutine label can be reused for different press count values, when the action it performs depends on the actual press count. Therefore, making the PressCount variable global (instead of Static) is often advantageous.

Here is an even compacter version of valenock's script, using only 5 AHK commands:
Code:
+F1 UP::CountPresses("KEY0","M2","Mx")

KEY0:        ; Send hotkey for original function
   SendInput % RegExReplace(A_ThisHotKey,"[\*\~\$]*([\#\+\!\^]*)(.+)( UP)","$1{$2}")
Return
M2:
   MsgBox Double
Return
Mx:
   MsgBox Counts = %PressCount%
Return

CountPresses(Labl1, Labl2, Labl3) {
   Global PressCount ; useful outside
   PressCount := A_ThisHotKey = A_PriorHotKey && A_TimeSincePriorHotkey < 400 ? PressCount+1 : 1
   KeyWait % RegExReplace(A_ThisHotKey,"[\*\~\$\#\+\!\^( UP)]"), DT0.4
   If (ErrorLevel)
      GoSub % PressCount < 3 ? Labl%PressCount% : Labl3
}
The Hotkey is Shift-F1. It should work also with scan codes, normal keys or special keys, like LEFT or ENTER.
Back to top
View user's profile Send private message
jonny



Joined: 13 Nov 2004
Posts: 3004
Location: Minnesota

PostPosted: Sun Feb 18, 2007 1:34 am    Post subject: Reply with quote

Quote:
Here is an even compacter version of valenock's script, using only 5 AHK commands:


Personally I would be a lot more impressed if it were readable and elegant.
Back to top
View user's profile Send private message
Laszlo



Joined: 14 Feb 2005
Posts: 4012
Location: Pittsburgh

PostPosted: Sun Feb 18, 2007 2:40 am    Post subject: Reply with quote

jonny wrote:
I don't think it would be simpler...

jonny wrote:
Personally I would be a lot more impressed if it were readable and elegant.
It is hard to satisfy you Wink A solution with one thread is simpler and more readable than the original with a second thread started inside a function.

I think your problem is that regular expressions are cryptic to the untrained eyes. I agree. You should complain to their developers. The other four lines are readable. If they are elegant? It is in the eyes of the beholder. Could you give a description of what you consider elegant? I will write a version just for you. Smile

There is also a serious weakness of the original approach (regardless of the complexity of the mixed-up scopes of the variables in the timer subroutine and the surrounding function): all the key presses must finish within 400ms from the first key press. You cannot extend the function to handle more key presses, because the 400ms will expire in the middle of the key sequence. It is much better to allow 400ms for each key press, so you can reliably detect even 20 repetitions. This is, how the timer-less function works.
Back to top
View user's profile Send private message
Laszlo



Joined: 14 Feb 2005
Posts: 4012
Location: Pittsburgh

PostPosted: Sun Feb 18, 2007 4:03 am    Post subject: Reply with quote

In the original function you have to use unintuitive assignments to static variables and rely on the funny behavior of GoSub %label%, just to hide some internal variables. If you don't mind using globals, you can simplify the timer based function (and allow 400ms delay between keystrokes):
Code:
$+F1::CountPresses("KEY0","M2","Mx") ; use $ with a KEY0 label, or #usehook

KEY0:        ; Send hotkey for original function
   SendInput % RegExReplace(A_ThisHotKey,"[\*\~\$]*([\#\+\!\^]*)(.+)( UP)*","$1{$2}")
Return
M2:
   MsgBox Double
Return
Mx:
   MsgBox Counts = %PressCount%
Return

CountPresses(param1, param2, param3) {
   Global
   pressed1 := param1, pressed2 := param2, pressed3 := param3
   PressCount += 1
   SetTimer WaitKeyPress, 400 ; Restart waiting for more presses
}

WaitKeyPress:
   SetTimer WaitKeyPress, off ; run timer only once
   GoSub % PressCount < 3 ? pressed%PressCount% : pressed3
   PressCount := 0
Return
Here the function and the timer subroutine is separated. This way the scopes of variables don't mix up, the code becomes clearer, more readable.

Edit 20070118: made UP optional in KEY0


Last edited by Laszlo on Sun Feb 18, 2007 5:55 pm; edited 1 time in total
Back to top
View user's profile Send private message
Titan



Joined: 11 Aug 2004
Posts: 5068
Location: imaginationland

PostPosted: Sun Feb 18, 2007 11:53 am    Post subject: Reply with quote

Great idea. Here's a similar version that simply returns the number of key presses in the given time:

Code:
If CountPresses("LButton") = 2
   MsgBox, you double clicked

press := CountPresses("a", 3000)
MsgBox, you pressed 'a' %press% times in 3 seconds
; -----

CountPresses(key, delay = 0, presses = 0) {
   If !delay and DllCall("SystemParametersInfo", "UInt", 22 ; SPI_GETKEYBOARDDELAY
      , "UInt", 0, "UIntP", delay)
      delay *= 250 + 250
   timeout := delay / 1000, start := A_TickCount
   Loop
      If (presses++ and A_TickCount - start > delay)
         Return, --presses - ErrorLevel
      Else {
         KeyWait, %key%, T%timeout%
         KeyWait, %key%, T%timeout% D L
      }
}

_________________

RegExReplace("irc.freenode.net/ahk", "^(?=(.(?=[\0-r\[]*((?<=\.).))))(?:[c-\x73]{2,8}(\S))+((2)|\b[^\2-]){2}\D++$", "$u3$1$3$4$2")
Back to top
View user's profile Send private message Visit poster's website
Laszlo



Joined: 14 Feb 2005
Posts: 4012
Location: Pittsburgh

PostPosted: Sun Feb 18, 2007 5:13 pm    Post subject: Reply with quote

A few questions, Titan:
- Why do you default to 500 times to the system keyboard delay? Was not it better to set the default delay to 1000 or 500 in the parameter list?
- Did 250+250 (instead of 500) remained from a previous version?
- The DllCall returns an error in my XP SP2 system (A-4). Is there a problem with the call?
- Still, delay is set to 1 by the dll call. Should not it be 30ms or something similar?
- Because of the fix time window, it is hard to achieve large press counts. Is not it better to allow a delay between consecutive presses, and stop counting if there is a longer pause?

Still, it is a useful function. You can use it like
Code:
^#a::
   press := CountPresses("a")
   MsgBox, you pressed 'a' %press% times ; or any action you want
Return

The function below finds the hotkey itself, making it a little more convenient to use. It does not count the hotkeys in a fixed time interval, but until they are pressed without long pause in between:
Code:
+F1 UP::
^#a::MsgBox % "you pressed the hotkey " CountPresses() " times"

CountPresses(timeout = 0.4) {
   key := RegExReplace(A_ThisHotKey,"[\*\~\$\#\+\!\^( UP)]")
   Loop {
      KeyWait %key%, T%timeout%
      KeyWait %key%,DT%timeout%
      If (ErrorLevel)
         Return A_Index
   }
}
Back to top
View user's profile Send private message
sashabe



Joined: 09 Oct 2006
Posts: 14

PostPosted: Sun Feb 18, 2007 8:57 pm    Post subject: Reply with quote

Thanks a lot, invaluable scripts! Multiple presses should be incorporated into AHK engine, it seems, to lighten code.
Could please the author make the script insensible to long pressings, in the way the shift key is held for selecting text? I tried to attach a delete action to the shift double-pressed, but its common functionality stopped working..
Back to top
View user's profile Send private message
Richard B.



Joined: 27 Feb 2008
Posts: 10

PostPosted: Wed May 21, 2008 8:21 pm    Post subject: Reply with quote

does it work for mouse??
Back to top
View user's profile Send private message
infogulch



Joined: 27 Mar 2008
Posts: 127
Location: KC, MO

PostPosted: Fri May 23, 2008 5:25 pm    Post subject: Reply with quote

This one gives you the option of up to four labels/presses. It immediately fires the last most label you specify on encountering that press. What this means is you can specify nothing (blank "") for labels, and that number of presses won't do anything. Also, if you specify a space, (" ") instead of blank for the first label, it sends the current hotkey's down-event. (e.g. the hotkey "a::" would send "{a Down}". Make it send an up event with "$a Up::Send {a Up}" and remember to include the dollar sign) This is nice to allow the first press to go through, but subsequent presses are captured by the program, and not sent to the window.:

Code:
;Note: The MTime first paramater is milliseconds. I find 150-200 about right for pressing several times. Also, the MTime is timing between strokes, not capturing how many strokes in a set time. This makes it easier on you since you don't have to change the time if you specify more or less labels.

CountPresses( MTime = 150, Label1 = "", Label2 = "", Label3 = "", Label4 = "") {
   If ( Label1 = " " ) {
      SendKey := RegExReplace(A_ThisHotKey,"i)([\*\~\$( UP)])?(.*?)(\w+)", "$2{$3 Down}")
      Send, %SendKey%
   }
   Time := MTime * .001
   LastLabel := IsLabel( Label4 ) ? 4 : IsLabel( Label3 ) ? 3 : IsLabel( Label2 ) ? 2 : 1
   Key := RegExReplace(A_ThisHotKey,"i)[\*\~\$\#\+\!\^( UP)]")
   AddPress:
   Presses += 1
   KeyWait, %Key%, T%Time%
   If ( Presses < LastLabel AND !ErrorLevel) {
      KeyWait, %Key%, D T%Time%
      If !ErrorLevel
         GoTo AddPress
   }
   If IsLabel( Label%Presses% )
      Gosub, % Label%Presses%
   Return Presses
}

Ctrl::CountPresses(150, " ", "Second", "Third")
$Ctrl Up::Send {Ctrl Up}

First:
Second:
Third:
Fourth:
   On := !On
   If On
      ToolTip The label: "%A_ThisLabel%" ran
   Else
      ToolTip
return

It includes an example of useage using Ctrl (like google desktop) there at the end.
to fix:
I'd like to fix how it removes the Autorepeat of your keyboard if you use the space " " option. (the repeat changes to whatever mtime you set.)
Maybe change {$2 Down} to ..DownTemp}?? i don't know what difference that would make, but there's something about that in the Remapping Keys and Buttons
_________________
Back to top
View user's profile Send private message
Display posts from previous:   
Post new topic   Reply to topic    AutoHotkey Community Forum Index -> Scripts & Functions All times are GMT
Page 1 of 1

 
Jump to:  
You can post new topics in this forum
You can reply to topics in this forum


Powered by phpBB © 2001, 2005 phpBB Group