TapHoldManager icon indicating copy to clipboard operation
TapHoldManager copied to clipboard

Can we get support for Autohotkey V2 please?

Open gaberiel44 opened this issue 2 years ago • 6 comments

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.

gaberiel44 avatar Feb 04 '23 06:02 gaberiel44

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)
		}
	}
}

Drei109 avatar Aug 08 '23 02:08 Drei109

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

trACEroam avatar Jan 21 '24 09:01 trACEroam

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.

ChaliceChore avatar Mar 30 '24 12:03 ChaliceChore

@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 avatar Apr 02 '24 01:04 evilC

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

evilC avatar Apr 02 '24 02:04 evilC

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

evilC avatar Apr 02 '24 02:04 evilC