evilC / TapHoldManager

An AHK library for Long Press / Multi tap / Multi tap and hold
MIT License
124 stars 13 forks source link

Can we get support for Autohotkey V2 please? #11

Open gaberiel44 opened 1 year ago

gaberiel44 commented 1 year ago

I am currently trying to move over to V2, TapHoldManager is core to my Autohotkey code base. So I am stuck with V1.

I am not much of a programmer, I find V1 to be a pain to use. I tested V2 and it solves many of my gripes with V1.

Can we please get a V2 port. It does not have to be any time soon. But I would appreciate to know that its on your road map.

Thanks for this library.

Drei109 commented 1 year ago

Someone did it on the forums: https://www.autohotkey.com/boards/viewtopic.php?p=525416#p525416

class TapHoldManager {
    Bindings := Map(), Bindings.CaseSense := "Off"

    __New(tapTime := 150, holdTime := tapTime, maxTaps := -1, prefixes := "$", window := ""){
        this.tapTime := tapTime
        this.holdTime := holdTime
        this.maxTaps := maxTaps
        this.prefixes := prefixes
        this.window := window
    }

    Add(keyName, callback, tapTime?, holdTime?, maxTaps?, prefixes?, window?){    ; Add hotkey
        if this.Bindings.Has(keyName)
            this.RemoveHotkey(keyName)
        this.Bindings[keyName] := TapHoldManager.KeyManager(keyName, callback, tapTime ?? this.tapTime, holdTime ?? this.holdTime, maxTaps ?? this.maxTaps, prefixes ?? this.prefixes, window ?? this.window)
    }

    RemoveHotkey(keyName){ ; to remove hotkey
        this.Bindings.Delete(keyName).SetState(0)
    }

    PauseHotkey(keyName){ ; to pause hotkey temprarily
        this.Bindings[keyName].SetState(0)
    }

    ResumeHotkey(keyName){ ; resume previously deactivated hotkey
        this.Bindings[keyName].SetState(1)
    }

    class KeyManager {
        state := 0                  ; Current state of the key
        sequence := 0               ; Number of taps so far
        holdActive := 0             ; A hold was activated and we are waiting for the release

        __New(keyName, Callback, tapTime, holdTime, maxTaps, prefixes, window){
            this.keyName := keyName
            this.Callback := Callback
            this.tapTime := tapTime
            this.holdTime := holdTime
            this.maxTaps := maxTaps
            this.prefixes := prefixes
            this.window := window

            this.HoldWatcherFn := this.HoldWatcher.Bind(this)
            this.TapWatcherFn := this.TapWatcher.Bind(this)
            this.JoyWatcherFn := this.JoyButtonWatcher.Bind(this)
            this.DeclareHotkeys()
        }

        DeclareHotkeys(){
            if (this.window)
                HotIfWinactive this.window ; sets the hotkey window context if window option is passed-in

            Hotkey this.prefixes this.keyName, this.KeyEvent.Bind(this, 1), "On" ; On option is important in case hotkey previously defined and turned off.
            if (this.keyName ~= "i)^\d*Joy"){
                Hotkey this.keyName " up", (*) => SetTimer(this.JoyWatcherFn, 10), "On"
            } else {
                Hotkey this.prefixes this.keyName " up", this.KeyEvent.Bind(this, 0), "On"
            }

            if (this.window)
                HotIfWinactive ; restores hotkey window context to default
        }

        SetState(state){ ; turns On/Off hotkeys (should be previously declared) // state is either "1: On" or "0: Off"
            ; "state" under this method context refers to whether the hotkey will be turned on or off, while in other methods context "state" refers to the current activity on the hotkey (whether it's pressed or released (after a tap or hold))
            if (this.window)
                HotIfWinactive this.window

            state := (state ? "On" : "Off")
            Hotkey this.prefixes this.keyName, state
            if (this.keyName ~= "i)^\d*Joy"){
                Hotkey this.keyName " up", state
            } else {
                Hotkey this.prefixes this.keyName " up", state
            }

            if (this.window)
                HotIfWinactive
        }

        JoyButtonWatcher(){
            if GetKeyState(this.keyName)
                return
            SetTimer this.JoyWatcherFn, 0
            this.KeyEvent(0)
        }

        KeyEvent(state, *){
            if (state == this.state)
                return  ; Suppress Repeats
            this.state := state
            if (state){
                ; Key went down
                this.sequence++
                SetTimer this.HoldWatcherFn, -this.holdTime
            } else {
                ; Key went up
                SetTimer this.holdWatcherFn, 0
                if (this.holdActive){
                    this.holdActive := 0
                    SetTimer this.FireCallback.Bind(this, this.sequence, 0), -1
                    this.sequence := 0
                    return
                }
                if (this.maxTaps > 0 && this.Sequence == this.maxTaps){
                    SetTimer this.tapWatcherFn, 0
                    SetTimer this.FireCallback.Bind(this, this.sequence, -1), -1
                    this.sequence := 0
                } else {
                    SetTimer this.tapWatcherFn, -this.tapTime
                }
            }
        }

        ; If this function fires, a key was held for longer than the tap timeout, so engage hold mode
        HoldWatcher(){
            if (this.sequence > 0 && this.state == 1){
                ; Got to end of tapTime after first press, and still held.
                ; HOLD PRESS
                SetTimer this.FireCallback.Bind(this, this.sequence, 1), -1
                this.holdActive := 1
            }
        }

        ; If this function fires, a key was released and we got to the end of the tap timeout, but no press was seen
        TapWatcher(){
            if (this.sequence > 0 && this.state == 0){
                ; TAP
                SetTimer this.FireCallback.Bind(this, this.sequence), -1
                this.sequence := 0
            }
        }

        FireCallback(seq, state := -1){
            this.Callback.Call(state != -1, seq, state)
        }
    }
}
trACEroam commented 7 months ago

I have multiple scripts for THM and they are all #included in a single .ahk script. frustratingly, i discovered i can't run multiple scripts at the same time....if i want to run a particular script i have to either locate its .ahk file right click and Run Script or go to the tray icon and Reload This Script. How do I fix this

ChaliceChore commented 4 months ago

v2 doesn't work for me.

#requires AutoHotkey v2.0
#include <TapHoldManager>

thm := new TapHoldManager(100, 200, 1)
thm.Add("1" , Send("!"))

On running this AHK displays this error:

Warning: This variable appears to never be assigned a value. Specifically: global new

Removing new hides this error. But a new error is displayed when I press 1:

Error: This value of type "String" has no method named "Call".

Edit: Updated the code to this:

thm := TapHoldManager()
thm.Add("1" , MyFunc1 , 100 , 200 , 1)

MyFunc1() {
    Send("!")
}

New error is displayed by AHK:

Error: Too many parameters passed to function.

evilC commented 4 months ago

@iamMG this is because your callback function does not accept the parameters which tell you whether it was a tap or a hold Even if you don't use them, it must declare them

MyFunc1(isHold, taps, state) { ; <- Function MUST declare isHold, taps, and state!
    Send("!")
}
evilC commented 4 months ago

@trACEroam not really sure what you mean. If you supply some example scripts that exhibit the issue I could maybe help

evilC commented 4 months ago

Update: I am currently working on a release of THM for AHK v2 A WIP version can be found here It should be identical in usage (Apart from obviously not having to wrap your function names in Func()), except for one minor change. In v1, if you wanted to skip an optional parameter, you used -1 (eg thm.Add("1" , Func("MyFunc1"), -1, -1 , 2). In v2, you just omit the parameter (eg thm.Add("1" , MyFunc1 ,,, 2)) I could have kept using -1, but with v2's support for unset parameters and coalescing operators, it just felt a lot cleaner (And consistent with AHK's normal syntax) to make this change