Jump to content

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

MultiTap() - Fit many hotkeys into one


  • Please log in to reply
15 replies to this topic
berban
  • Members
  • 202 posts
  • Last active: Apr 12 2019 01:08 AM
  • Joined: 30 Dec 2009
MultiTap() - Fit many hotkeys into one



When I started with AutoHotkey, I followed Chris's example for multi-tap hotkeys as demonstrated in Example #3 of the SetTimer documentation. While the example given is good to demonstrate the possibilites of settimer, it's is a terribly clunky way of going about doing things with several hotkeys. However, I only just got around to improving the implementation.
If you have use multi-tap hotkeys, I would recommend looking at this function. As a matter of fact, I would recommend it to anyone who uses more than a couple hotkeys, as it can help you crowd a lot more hotkeys into your keyboard layout :)


Posted ImagePosted ImagePosted Image


Posted ImagePosted ImagePosted ImagePosted ImagePosted ImagePosted ImageMultiTap(CmdListCSV="", Delay=400)


Usage: Chris's multi-tap example is pasted below:
#c::
if winc_presses > 0 ; SetTimer already started, so we log the keypress instead.
{
	winc_presses += 1
	return
}
; Otherwise, this is the first press of a new series. Set count to 1 and start
; the timer:
winc_presses = 1
SetTimer, KeyWinC, 400 ; Wait for more presses within a 400 millisecond window.
return

KeyWinC:
SetTimer, KeyWinC, off
if winc_presses = 1 ; The key was pressed once.
{
	Run m:\  ; Open a folder.
}
else if winc_presses = 2 ; The key was pressed twice.
{
	Run m:\multimedia  ; Open a different folder.
}
else if winc_presses > 2
{
	MsgBox Three or more clicks detected.
}
; Regardless of which action above was triggered, reset the count to
; prepare for the next series of presses:
winc_presses = 0
return


To emulate this behavior with MultiTap(), gather each of the result branches into a comma-separated string and pass that string to the first parameter of MultiTap():
#c::MultiTap("Run m:\,Run m:\multimedia,MsgBox Three or more clicks detected.")

Posted Imagehttps://ahknet.autoh...izontalLine.png

Parameters:[*:2s9oa52a]CmdListCSV: A standard comma-delimited string into which you enter your commands. The first item will be the command executed upon first tap, the second item will be executed upon second tap, etc.
Currently, you have to define each of your commands individually (see the code for more details.) This should be fixed when V2 comes out, as you will be able to dynamically call any command.
If the function does not recognize your command, it will try going to a label, then running a function, then running a file or url, after which it will display an error saying the command could not be interpreted. This allows you to omit the command sometimes.
Optionally, you can also give each command a more friendly label. To do this, precede the command with a label name surrounded with colons like :My Command Name:Run C:\Docs[*:2s9oa52a]
Delay: The number of milliseconds to allow between taps.
https://ahknet.autoh...izontalLine.png

Why use MultiTap()?[*:2s9oa52a]Fit multiple related commands into the same hotkey
[*:2s9oa52a]Much more compact than the usual way of doing this; only needs one line
[*:2s9oa52a]Gives feedback with tooltips so you can select the correct option
[*:2s9oa52a]Soon any function will allow dynamic callshttps://ahknet.autohotkey.com/~berban/spacer.pnghttps://ahknet.autohotkey.com/~berban/spacer.pnghttps://ahknet.autohotkey.com/~berban/HorizontalLine.png

Some working examples:
;The following example makes the Windows+E hotkey 5x as useful
#e::MultiTap(":My Computer:Run ::{20d04fe0-3aea-1069-a2d8-08002b30309d},:Recycle Bin:Run ::{645ff040-5081-101b-9f08-00aa002f954e},:Program Files:Run " A_ProgramFiles ",:Windows:Run " A_WinDir ",:Start Menu:Run " A_StartMenu)

https://ahknet.autoh...izontalLine.png

Code:
MultiTap(CmdListCSV="", Delay=400, DisplayFunc="") ;http://www.autohotkey.com/forum/viewtopic.php?p=478447
{
	Static Cache
	If (CmdListCSV <> "") {
		StringReplace, TempCSV, CmdListCSV, ", "", All
		If (@ := InStr(Cache, TempCSV ""","))
			Cache := SubStr(Cache, 1, @ - 13) ($ := SubStr("00000" A_TickCount + Delay, -9) SubStr("0" (n := SubStr(Cache, @ - 2, 2) + 1), -1) TempCSV) SubStr(Cache, @ + StrLen(TempCSV))
		Else
			Cache .= """" ($ := SubStr("00000" A_TickCount + Delay, -9) "01" TempCSV) """,", n := 1
		Loop, Parse, CmdListCSV, CSV
			If (A_Index = n) {
				Found := True
				If RegExMatch(A_LoopField, "^\s*:\K(?:[^:]|::)*(?=:)", Text)
					StringReplace, Text, Text, ::, :, All
				Else
					Text := A_LoopField
				If (DisplayFunc <> "") and IsFunc(Display)
					%DisplayFunc%(Text)
				Else
					ToolTip, %Text%
				Break
			}
	}
	If !Found {
		If (DisplayFunc <> "") and IsFunc(DisplayFunc)
			%DisplayFunc%()
		Else
			ToolTip
		If (CmdListCSV <> "")
			StringReplace, Cache, Cache, "%$%"`,
		n := 0
	}
	If (CmdListCSV = "") {
		Loop, Parse, Cache, CSV
			If (A_LoopField <> "") and ((ThisTime > @ := SubStr(A_LoopField, 1, 10)) or (ThisTime = ""))
				ThisTime := @, ThisNum := SubStr(A_LoopField, 11, 2), ThisCmd := SubStr(A_LoopField, 13)
		Loop, Parse, ThisCmd, CSV
			If (A_Index = ThisNum)
				If (A_LoopField <> "") {
					RegExMatch(A_LoopField, "s)^\s*(?::(?:[^:]|::)*: ?)?\K(?P<Name>\w*)(?:\((?P<Func>.*?)\)|,? ?(?P<Input>.*))$", Cmd)
					If (CmdFunc <> "") { ; Will attempt to use a function if you used function notation, i.e. Command(Input1,Input2,Input3)
						If IsFunc(CmdName) {
							Loop, Parse, CmdFunc, CSV
								CmdFunc%A_Index% := A_LoopField
							%CmdName%(CmdFunc1, CmdFunc2, CmdFunc3, CmdFunc4, CmdFunc5, CmdFunc6)
						} Else
							MsgBox, 262160, %A_ScriptName% - %A_ThisFunc%(): Error, Invalid command!`n"%Cmd%"
					} Else If (CmdName = "Run")
						Run, %CmdInput%, , UseErrorLevel
					Else If (CmdName = "Send")
						Send, %CmdInput%
					Else If (CmdName = "SendInput")
						SendInput, %CmdInput%
;	If you like, follow the example of the previous 2 lines to add more commands to MultiTap(). %CmdName% will contain the name of the command and %CmdInput% will contain the additional string that is passed as a parameter. Below gives an example of adding message box functionality.
;					Else If (CmdName = "MsgBox")
;						MsgBox, %CmdInput%
					Else If (CmdName = "Goto") or (CmdName = "Gosub")
						SetTimer, %CmdInput%, -1
					Else If IsLabel(RegExReplace(Cmd, "\s"))
						SetTimer, % RegExReplace(Cmd, "\s"), -1
					Else If IsFunc(CmdName)
						%CmdName%(CmdInput)
					Else {
						Run, %Cmd%, , UseErrorLevel
						If ErrorLevel
							MsgBox, 262160, %A_ScriptName% - %A_ThisFunc%(): Error, Invalid command!`n"%Cmd%"
					}
					Break
				}
		StringReplace, ThisCmd, ThisCmd, ", "", All
		StringReplace, Cache, Cache, "%ThisTime%%ThisNum%%ThisCmd%"`,
	}
	Loop, Parse, Cache, CSV
		If (A_LoopField <> "") and ((NextTime > @ := SubStr(A_LoopField, 1, 10)) or (NextTime = ""))
			NextTime := @
	If NextTime
		SetTimer, MultiTap, % 0 > (@ := A_TickCount - NextTime) ? @ : -1
	Return n
	MultiTap:
	Return MultiTap()
}

https://ahknet.autoh...izontalLine.png

Notes:[*:2s9oa52a]Multiple lines of code are not yet supported for a single action. (An example of this would be Run app, WinWaitActive appwin, WinMaximize) I thought about it but I decided it would probably not be practical anyway. And at any rate it would be best to wait until V2
[*:2s9oa52a]Be careful about using the first optional comma with your commands, as the command lists are comma-separated.
~ Created with Quick Functions for Forums by berban ~

Find me on the new AutoHotkey forums and send me a message if you have a question about any of the scrips I've posted to this forum!


engunneer
  • Moderators
  • 9162 posts
  • Last active: Sep 12 2014 10:36 PM
  • Joined: 30 Aug 2005
this looks useful, and will answer a very commonly asked question.

Thanks!

nimda
  • Members
  • 4368 posts
  • Last active: Aug 09 2015 02:36 AM
  • Joined: 26 Dec 2010
Why use this rather than Morse() by Laszlo?

TheDewd
  • Members
  • 842 posts
  • Last active: Jun 10 2016 06:55 PM
  • Joined: 28 Mar 2010

Why use this rather than Morse() by Laszlo?

I think it's easier to use.

nimda
  • Members
  • 4368 posts
  • Last active: Aug 09 2015 02:36 AM
  • Joined: 26 Dec 2010
OK, so it's easier to use, but it lacks the functionality of Morse(). With Morse, on one line, I can run different programs, like with this. However, I can also do multi-line stuff. In fact, I can even differentiate between short and long taps if I so choose.

Does this offer anything that Morse doesn't?

berban_
  • Members
  • 202 posts
  • Last active: Aug 05 2014 11:52 PM
  • Joined: 16 Mar 2011
Honestly, I don't really think this is very similar at all to morse... :?

How can you do all of the same things with morse? Best case scenario is to use an ugly ternary:

#e::Run, % (t := Morse()) = "0" ? "My Computer" : t = "1" ? "Recycle Bin" : t = "01" ? ...

But that has the disadvantage of only being able to do one command - the one that starts the line, in this case "Run".
MultiTap has some other advantages as well:[*:2ufi3166]Tooltip feedback[*:2ufi3166]You can go around again if you miss your option[*:2ufi3166]Doesn't hang up the script since it is a timer (though granted it's highly unlikely that this will ever factor in to anything)[*:2ufi3166]Doesn't need to be called by a hotkey (ditto)
I agree that multi-line options are better for morse, but that wasn't the goal of this particular function. I'd say that 85% of all my hotkeys are one-liners, the majority of those serving to run a file or website and the rest serving to select an odd menu item with keystrokes. That was the target here.

And if you're saying, well, why is this so complex when morse is 12 lines? Could I have done it more simply with KeyWait? Yeah probably :oops: but I don't know, I like this way a bit better I guess. Probably not worth the effort but hey, it's a sunk cost :p

berban_
  • Members
  • 202 posts
  • Last active: Aug 05 2014 11:52 PM
  • Joined: 16 Mar 2011
Edit - silly me, you CAN have multi-line hotkeys! You just have to use the "Goto" option.

Here's an example I just made:

!^-::MultiTap(":Bass Boost - ON:Goto BassBoostOn,:Bass Boost - OFF:Goto BassBoostOff")

[color=red]BassBoostOn[/color]:
[color=red]BassBoostOff[/color]:
Run, Resources\Sound Properties.lnk ;shortcut to control panel item
;...
;some more winactivate's and stuff to get to the right dialog box
;...
SendInput % "{tab}{home}" [color=orange](A_ThisLabel = "BassBoostOn" ? "{down 2}" : "")[/color] "{enter}"
;...
;exits
;...
Return

...although I forgot that you can't actually use goto from within a function, so I changed the goto command to actually be SetTimer, Label, -1 - which does pretty much the same thing. So make sure you change that or copy the code again

vahju
  • Members
  • 337 posts
  • Last active: Sep 21 2014 03:52 AM
  • Joined: 17 Feb 2008
ahkL on win7 w/ admin rights

I tried running the below code

NumPadDiv::MultiTap("Send {NumPadDiv},Send {shift down}9{shift up}",400)
NumPadMult::MultiTap("Send {NumPadMult},Send {shift down}0{shift up}",400)

I would get the tooltip for each but the first tap would not result in anything.

Also how would you incorporate sendraw.

Also is there anyway to get rid of the tool tips? Maybe add another parameter to the function to keep tooltips.

berban_
  • Members
  • 202 posts
  • Last active: Aug 05 2014 11:52 PM
  • Joined: 16 Mar 2011

I tried running the below code

NumPadDiv::MultiTap("Send {NumPadDiv},Send {shift down}9{shift up}",400)
NumPadMult::MultiTap("Send {NumPadMult},Send {shift down}0{shift up}",400)


That's probably because your hotkey is sending the same key as the hotkey itself. Use the $ symbol to fix that (See the Hotkeys documentation)

[color=red]$[/color]NumPadDiv::MultiTap("Send {NumPadDiv},Send {shift down}9{shift up}",400)
[color=red]$[/color]NumPadMult::MultiTap("Send {NumPadMult},Send {shift down}0{shift up}",400)

As for Send raw, that can be easily incorporated into the existing design:
$NumPadDiv::MultiTap("Send [color=red]{Raw}[/color]{NumPadDiv},Send [color=red]{Raw}[/color]{shift down}9{shift up}",400)
(...although this would mess up both your hotkeys! :p )

Finally: yes, you can easily add another parameter to disable tooltips.

Edit - see next post / original code

nimda
  • Members
  • 4368 posts
  • Last active: Aug 09 2015 02:36 AM
  • Joined: 26 Dec 2010
FYI, labels can go inside functions.

vahju
  • Members
  • 337 posts
  • Last active: Sep 21 2014 03:52 AM
  • Joined: 17 Feb 2008
@berban_ thanks for the assist with $ symbol. Totally forgot about it.

The reason I asked for SendRaw is so I could just use ( or ) and not spell out all the hotkeys.

Also the non ToolTip option is great. I just thought up another idea for tool tips. Have the parameter feed the number of the tooltip you want.

So if I had 3 actions set to a hotkey but I only want to see the tooltip for the last 2 actions I would send the parameter ToolTip=23. If I leave the parameter blank it sends tooltip for all. If I send the word false then there is no tooltip at all.

This is just an idea and its not something I need.

Thanks for this function and your quick response. It will make my coding of double and triple hotkeys much easier.

berban_
  • Members
  • 202 posts
  • Last active: Aug 05 2014 11:52 PM
  • Joined: 16 Mar 2011
vahju - Better yet, I made a tiny fix in the original code to incorporate both of your desires :)

Here's what you do now: So the optional tooltip label is between colons before the command. Just indicate an empty label for a particular command and there will be no tooltip. Here's an example.
Action1 = ::Run www.google.com
Action2 = :Mah videos!:Run www.YouTube.com
Action3 = Send !{F4}
!p::MultiTap(Action1 "," Action2 "," Action3)
The first action has a blank label (::) so no tooltip will be displayed. The 2nd action has a custom non-blank label, which will be displayed in a tooltip. The 3rd has no label so a tooltip containing "Send !{F4}" will be displayed. So basically this is like Tooltip=23

XX0
  • Members
  • 32 posts
  • Last active: Dec 12 2014 01:05 PM
  • Joined: 17 Jul 2010
I'm a big fan of this function. A nice, clean, reusable function instead of my old block of code. But would it be difficult to make it compatible with key repeat? So lets say I have
a::MultiTap("::send a, ::send cool story bro")
Holding the 'a' button produces just a single 'a', without repeats.

berban_
  • Members
  • 202 posts
  • Last active: Aug 05 2014 11:52 PM
  • Joined: 16 Mar 2011
Hey XX0!

I have a couple of comments in response to your post; unfortunately none of them directly answer the question as far as I can tell :?

[*:36frkye4]If you're going to have your script sending "a", and "a" is also a hotkey in your script, you'll need the "$" operator (See Hotkeys)
[color=red]$[/color]a::MultiTap("::send a, ::send cool story bro")
[*:36frkye4]Your posted code also wouldn't have worked because you had a space after the first comma-separated item. But I updated the MultiTap() code to allow for this.
Now to try to address your original question: I'm not sure what you mean by 'holding the button'. If you just mean holding it down for a few seconds so it would normally repeat several times and type something like "aaaaaaaaaaaaaaaa" then that isn't really applicable to this function. It would just continue to cycle through your options and eventually choose one when you released the key, sort of like roulette. But of course that isn't very helpful for most people. Can you clarify your question a bit?

XX0
  • Members
  • 32 posts
  • Last active: Dec 12 2014 01:05 PM
  • Joined: 17 Jul 2010
My code example would indeed haven't worked, it was just an example from the top of my head for clarification... which kind of failed.

Continuing on the example, getting an output of 'aaaaaaaaa' would be my goal. I can see your point on the roulette style of picking options, but this could be overcome by explicitly checking key-up events. Since key repeat only produces key-down event without the key-up event a distinction between the two could be made.

It also just occurred to me that that detection of held buttons might not be relevant or desirable for a function called MultiTap :oops:.

Anyway, I threw the following code together just for my repeatable key:
If (A_PriorHotKey = A_ThisHotkey and A_TimeSincePriorHotkey < 200 and A_TimeSincePriorHotkey > 75)
;double tap
else
;single tap or key repeat