[Class] MultiHotkeys - Easily define double press hotkeys (or triple, quadruple, etc)

Post your working scripts, libraries and tools for AHK v1.1 and older
evilmanimani
Posts: 29
Joined: 24 Jun 2020, 16:42

[Class] MultiHotkeys - Easily define double press hotkeys (or triple, quadruple, etc)

Post by evilmanimani » 14 Oct 2021, 18:41

This Class uses the input hook to allow for easy configuration of multi-hotkeys (or single hotkeys if you want). It will account for multiple input strings set to the same hotkey, and in those cases ({F1}{F1} vs {F1}{F1}{F1}), it will trigger the one you most recently entered after a configurable timeout. This should make it easier to manage when configuring without the need of Settimer, KeyWait, or A_TimeSincePriorHotkey checks.

Needs some testing, so please let me know if you have any suggestions or feedback.
Class_MultiHotkeys.ahk
(6.97 KiB) Downloaded 61 times
Example script:

Code: Select all

; Example Script:
#Persistent
#NoEnv
#SingleInstance, force
SetTitleMatchMode, RegEx
SetBatchLines, -1
mk := new MultiHotkeys()

; Use hotkey command before definition to make context sensitive:
; Hotkey, IfWinActive, Notepad

mk.Add("d"                  , "t200" , "d")      ; send d if the below times out
mk.Add("dd"                 ,        , "Test"    , "Double hotkey") ; Calling functions
mk.Add("ddd"                ,        , "Test"    , "Triple hotkey!")
mk.Add("dddd"               ,        , "Test"    , "Quadruple hotkey!!")
mk.Add("{F1}{F1}"           ,        , "Test2")  ; Calling a label; F1 > F1
mk.Add("^{F1}{F1}"          ,        , "Test3")  ; Sending a string; Ctrl+F1 > F1
mk.Add("+t"                 ,        , "T")      ; Capital T if Shift+t times out
mk.Add("+thisisatest"       , "t1000", "Test"    , "Hello,"  ,"World!") ; Sending 2 parameters ; Shift+t > hisisatest
mk.Add("#{Capslock}{F2}{F3}", "t800" , "Test"    , "Wow, this is a long hotkey!") ; Win+Caps > F2 > F3
return

Test(param1:="",param2:="") {
    global mk
    str := mk.inputStr
    if param1
        str .= "`r`n" param1
    if param2
        str .= "`r`n" param2
    tooltip, % str
    SetTimer, Tooltipoff, -3000
    return
    TooltipOff:
    tooltip
    return
}

Test2:
msgbox, % A_ThisLabel "`r`n" mk.inputStr
return
The class:

Code: Select all

;---------------------------------------------------------------------------------------------------------------------------------------
;
;   Class_Multihotkeys.ahk v0.1 by evilmanimani
;
;
;   Easily set up double, triple, or more hotkeys, and more for pseudo hotstrings, with a configurable timeout
;   Can go functions with parameters, labels, or just send characters if it doesn't match a function/label
;   If two input strings are similar i.e. (!aa, !aaa, !aaaa), it will trigger the shorter ones after the timeout
;   , the longest will be triggered immediately.
;
;   mh := new MultiHotkeys() to start
;   
;   The only public method is Add, see examples for details:
;   
;       - keys:        string of letter keys, optionally starting with modifiers the modifier only needs to be held for the first key
;                      should support ~, but not really tested.
;       - options:     at the moment, only supports a timeout in milliseconds, this is the timeout period between each keypress
;                      of the full input string, entered as t400, t1000, etc; defaults to 400ms; the timeout applies to any configured
;                      input string that shares the same starting hotkey (the modifiers and first character)
;       - function:    either a function or label name, any other text not matching a function onr label will be sent as-is
;       - params:      if passing a Function, any amount of associated params are supported
;
;
Class MultiHotkeys {

    __New(options:="") { ; not much for options here yet
        this.ih := InputHook(Options)
        this.KeyDict := {}
    }

    Add(keys, options:="", function :="", params*) {
        static optLookup := {"T":"timeout","t":"timeout"}
        for i, opt in StrSplit(options, A_Space) {
            if optLookup.HasKey(SubStr(opt,1,1)) {
                var := optLookup[SubStr(opt,1,1)]
                %var% := SubStr(opt,2)
            }
        }
        RegExMatch(keys, "O)^(?<mods>[~!^+#]{1,4})?(?<keys>.*)", keys)
        mods := StrReplace(keys.mods,"~",,noHide)
        keyStr := keys.keys
        If IsFunc(function) {
            if params
                func := Func(function).Bind(params*)
            else
                func := Func(function)
            
        }
        if IsObject(keyStr) {
            ; to-do: put in support for simple remaps via passing an array, i.e. mp.Add({"aa":"b","bb":"c","cc":"d"},,"Test")
        } else {
            hotkeyFunc := ObjBindMethod(this,"HotkeyHandler")
            pos := 1
            loop {
                pos := RegExMatch(keyStr,"({.*?})|(\w)",key,pos)
                if (A_Index = 1) {
                    pos += StrLen(key)
                    firstKey := key
                    hk := ( noHide ? "~" : "$") . mods . RegExReplace(key, "[{}]")
                    HotKey, % hk, % hotkeyFunc
                } else {
                    this.ih.KeyOpt(key, "+E+S")
                    pos += StrLen(key)
                }
            } Until (!key)
            
            keyStr := StrReplace(keyStr, firstKey, , , 1)
            keyStr := RegExReplace(keyStr, "[{}]")
            if !isObject(this.KeyDict[hk]) {
                this.KeyDict[hk] := {}
                this.KeyDict[hk].timeout := timeout ? Format("{:.1f}", timeout / 1000) : 0.4
            }
            this.KeyDict[hk][keyStr] := {}
            this.KeyDict[hk][keyStr].function := func
            this.KeyDict[hk][keyStr].funcName := function
            if mods
                this.KeyDict[hk][keyStr].mods := mods
        }
    }

    HotkeyHandler() {
        Suspend, On
        thisHotkey := A_ThisHotkey
        Mods := RegExReplace(thisHotkey, "i)^[~\$]*([!^+#]{0,4}).*$", "$1")
        timeout := this.KeyDict[thisHotkey].timeout
        loop {
            matched := []
            this.ih.Start()
            EndReason := this.ih.Wait(timeout)
            inputStr .= this.ih.EndKey
            for k, v in this.KeyDict[thisHotkey] {
                if IsObject(v) {
                    if InStr(k, inputStr)
                        matched.Push(k)
                    maxLen := StrLen(k) > maxLen ? StrLen(k) : maxLen
                }
            }
            if ((matched.MaxIndex() = 1 && this.KeyDict[thisHotkey].HasKey(inputStr))
            || !EndReason || StrLen(inputStr) >= maxLen)
                break
        }
        this.ih.Stop()
        matchConfirm := []
        for i, e in matched {
            if this.KeyDict[thisHotkey][e].HasKey("mods") {
                for _, char in StrSplit(mods) {
                    if InStr(this.KeyDict[thisHotkey][inputStr].mods,char) {
                        matchConfirm.Push(e)
                        continue 2
                    }
                }
            } else if !mods {
                matchConfirm.Push(e)
            }
        }
        if ((matched.MaxIndex() = 0 && this.KeyDict[thisHotkey].HasKey(""))
            || (this.KeyDict[thisHotkey].HasKey(matchConfirm.1) && (!Endreason || matchConfirm.MaxIndex() = 1))) {
            this.inputStr := SubStr(thisHotkey,2) . inputStr
            funcName := this.KeyDict[thisHotkey][inputStr].funcName
            if IsFunc(funcName) {
                this.KeyDict[thisHotkey][inputStr].function.Call()
            } else if IsLabel(funcName) {
                Gosub, % funcName
            } else {
                Send, % "{raw}" funcName
            }
        } else {
            this.inputStr := ""
        }
        Suspend, Off
    }
}
https://github.com/evilmanimani/Class_Multihotkeys.ahk

huydc
Posts: 1
Joined: 15 Oct 2021, 22:14

Re: [Class] MultiHotkeys - Easily define double press hotkeys (or triple, quadruple, etc)

Post by huydc » 16 Oct 2021, 01:03

Thanks for the very nice script!
One suggestion:
Allow an option to press a > s > s > s to repeat a > s if a is still being held down
Regarding performance: I think the timeout is a little bit longer than what is specified ?
The default timeout is 400ms, but it feels like 800ms or 1s
I'm still testing but so far it's been great.

evilmanimani
Posts: 29
Joined: 24 Jun 2020, 16:42

Re: [Class] MultiHotkeys - Easily define double press hotkeys (or triple, quadruple, etc)

Post by evilmanimani » 16 Oct 2021, 12:26

I need to test the timing a bit more, right now the timeout resets for each key in the sequence which would give that appearance, would it make more sense for the timeout to apply to the entire sequence? And I can see what can be done regarding sending the key repeatedly, i.e., if you keep hitting the last key in the sequence while still holding the others, I was able to change the method for for detecting strings to a timer, so I'm hoping I can have it detect keyup events now.

Post Reply

Return to “Scripts and Functions (v1)”