[WIP] HotClass - Dynamic Hotkey and Binding system

Post your working scripts, libraries and tools for AHK v1.1 and older
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

[WIP] HotClass - Dynamic Hotkey and Binding system

25 Jan 2015, 16:19

WARNING: REQUIRES TEST BUILD OF AHK FROM HERE.
To easily swap in and out AHK version, use the EXE Swapper from here.
Also see dependencies - links are in the code.

About
I have managed (I think) to emulate (roughly) AHK's hotkey system without any Hotkey commands or a::b style remappings, using SetWindowsHookEx calls.
Currently it only supports keyboard and mouse, but this is just an early proof-of-concept.

A big thanks goes out to Lexikos, GeekDude, nnik, HotkeyIt, and anyone else who has helped me with this lately, it's been a journey!

Current Features
  • Supports Keyboard and Mouse input
  • Binding system detects all supported input in any combination and in any number.
    I successfully used all 10 fingers with two keyboards. n-key rollover is going to be the only limitation here.
  • End Key is important. The final key used to complete the combination identifies the combination.
    Hold A+B, hit C is not the same as hold C+B, hit A !
  • Commands to add hotkeys.
But be aware, this is very experimental, and subject to massive change. DO NOT base anything important on this code for now.

Ultimately, I plan to merge CHID into this code to provide HID device input as well as Keyboard and Mouse. Possibly even HID detection of buttons on mice etc that normally are a pain to map. However, HotkeyIt and I have to figure out the beast that is RawInput before that can happen.

Then, the ultimate plan is to merge that all into an app that can map anything to anything via little ahk scripts.
Want to remap a button? simple, load a button to button plugin, bind the input key, bind the output key, specify which app to take effect in, done - app will remap that key when in the specified program.
Want to map mouse to a joystick axis? no problem, load the relevant plugin, point it at your mouse, point it at a vjoy stick, and voila mouse to joystick. Want to invert one of the axes? Tick invert in the plugin or if it dont have it extend the plugin class and add an invert toggle.

The scope of all of this is pretty big, so if anyone wishes to help out, even if it is just with design suggestions, please feel free to pitch in.

Why do it?
  • AHK is limited to 1000 hotkeys. Not an issue for your average joe, but for the project I have in mind, a bit of a showstopper.
  • AHK does not support up events for joystick buttons, or event-based joystick axis polling (ie an event fires when the stick moves).
    Currently, this script does not support sticks in this way either - this will be added via HID input once I can get RawInput joystick capabilities enumerating (Anyone that can help with this, please get in touch!)
  • If you wish to use a "Dynamic Hotkey" system (ie user picks which hotkey to use for a script function by clicking a button in the script's GUI and hitting the desired key combo) then your options are very limited. The Hotkey GUI item is pretty much useless, and the best alternative method (Input command) requires hacky workarounds (#if directives and "End Key stuffing") and even then it doesn't support everything (eg joystick buttons).
    There is currently nothing in the lib to directly support this, but this is the thing I am currently working on.
How does it work?
A SetWindowsHookEx callback is set up which interrupts all keyboard and mouse input.

Bindings are added via a call to a class method, but using a totally different syntax to AHK.

The state of all keys are held in an object (Spec here), which also handles "variant" modifiers (ie if you hold "LControl", "Control" is also marked as being "down".

Upon any key event, the Bindings object (spec Here) is queried for matching entries.
Both wild (AHK *)and passthrough (AHK ~) are supported.
Matches are made in a "deepest" (most modifiers) to "shallowest" (least modifiers) order, and only one hotkey should ever match.

If a match is found, the bound function is called.

If the "passthru" param (AHK equivalent: ~ prefix) is not set, SetWindowsHookEx is told to block the input.

The Test Script
Hit F12 to test the binding system. It will block all button presses and mouse clicks and attempt to detect what key combo you hit.

Hotkeys can be added through code, some are provided in the test script but are commented out. Try enabling them and see how they work for you. Some are at the start of the code, some are in the example class.
ctrl+a (passthru: off, wild: on)
ctrl+shift+a (passthru: off, wild: off)
b (passthru: on, wild: off)

All hotkeys make a beep when activated.
The beep is asynch.

HotClass Github Page

Design Documents
(Link to simple portable viewer in docs)
Bindings Object(Holds list of current bindings)
State Object (Holds state of inputs for matching to bindings)

I am just using one branch on github for now, so it may be broken. I will try to keep a relatively up-to-date working version below.

Code: Select all

; DEPENDENCIES:
; _Struct():  https://raw.githubusercontent.com/HotKeyIt/_Struct/master/_Struct.ahk - docs: http://www.autohotkey.net/~HotKeyIt/AutoHotkey/_Struct.htm
; sizeof(): https://raw.githubusercontent.com/HotKeyIt/_Struct/master/sizeof.ahk - docs: http://www.autohotkey.net/~HotKeyIt/AutoHotkey/sizeof.htm
; WinStructs: https://github.com/ahkscript/WinStructs

#include <_Struct>
#include <WinStructs>
/*
ToDo:
* Per-App settings
* _StateIndex, _Bindings dynamic properties - set 0 on unset.
* HID input for joystick support
* Allow removal of bindings
* Sanity check bindings on add (duplicates, impossible keys etc)
* Binding GUI Item?

Bugs:
* Win does not work as a modifier, but does as an end key !?

*/
#SingleInstance force
OnExit, GuiClose

HKHandler := new HotClass()

;fn := Bind("AsynchBeep", 1000)
;HKHandler.Add({type: HotClass.INPUT_TYPE_K, input: GetKeyVK("a"), modifiers: [{type: HotClass.INPUT_TYPE_K, input: GetKeyVK("ctrl")}], callback: fn, modes: {passthru: 0, wild: 1}, event: 1})
;fn := Bind("AsynchBeep", 500)
;HKHandler.Add({type: HotClass.INPUT_TYPE_K, input: GetKeyVK("a"), modifiers: [{type: HotClass.INPUT_TYPE_K, input: GetKeyVK("ctrl")},{type: HotClass.INPUT_TYPE_K, input: GetKeyVK("shift")}], callback: fn, modes: {passthru: 0}, event: 1})

mc := new CMainClass()

Return

F12::
	while (GetKeyState("F12", "P")){
		Sleep 20
	}
	HKHandler.Bind("BindingDetected")
	return


; Test Bind ended
; data holds information about the key that triggered exit of Bind Mode
BindingDetected(binding, data){
	global HKHandler
	human_readable := HKHandler.GetBindingHumanReadable(binding, data)
	s := "You Hit " human_readable.endkey
	if (human_readable.modifiers){
		s .= " while holding " human_readable.modifiers
	}
	ClipBoard := s
	msgbox % s
}

Esc::ExitApp
GuiClose:
ExitApp

; Asynchronous Beeps for debugging or notification
AsynchBeep(freq){
	fn := Bind("Beep",freq)
	; Kick off another thread and continue execution.
	SetTimer, % fn, -0
}

Beep(freq){
	Soundbeep % freq, 250	
}

; Test functionality when callback bound to a class method
Class CMainClass {
	__New(){
		global HKHandler
		fn := Bind(this.DownEvent, this)
		HKHandler.Add({type: INPUT_TYPE_K, input: GetKeyVK("b"), modifiers: [], callback: fn, modes: {passthru: 1}, event: 1})
	}
	
	DownEvent(){
		AsynchBeep(750)
	}
}

; Test script end ==============================

; Only ever instantiate once!
Class HotClass {
	_Bindings := []				; Holds list of bindings
	_BindMode := 0				; Whether we are currently making a binding or not
	_StateIndex := []			; State of inputs as of last event
	_BindModeCallback := 0		; Callback for BindMode
	_MAPVK_VSC_TO_VK := {}		; Holds lookup table for left / right handed keys (eg lctrl/rctrl) to common version (eg ctrl)
	_MAPVK_VK_TO_VSC := {}		; Lookup table for going the other way
	_MapTypes := { 0:"_MAPVK_VK_TO_VSC", 1:"_MAPVK_VSC_TO_VK"}	; VK to Scancode Lookup tables.
	
	static INPUT_TYPE_M := 0, INPUT_TYPE_K := 1, INPUT_TYPE_O := 2
	static MOUSE_WPARAM_LOOKUP := {0x201: 1, 0x202: 1, 0x204: 2, 0x205: 2, 0x207: 3, 0x208: 3, 0x20A: 6, 0x20E: 7} ; No XButton 2 lookup as it lacks a unique wParam
	static MOUSE_NAME_LOOKUP := {LButton: 1, RButton: 2, MButton: 3, XButton1: 4, XButton2: 5, Wheel: 6, Tilt: 7}
	static MOUSE_BUTTON_NAMES := ["LButton", "RButton", "MButton", "XButton1", "XButton2", "MWheel", "MTilt"]
	static INPUT_TYPES := {0: "Mouse", 1: "Keyboard", 2: "Other"}


	; USER METHODS ================================================================================================================================
	; Stuff intended for everyday use by people using the class.
	
	; Add a binding. Input format is basically the same as the _Bindings data structure. See Docs\Bindings Structure.json
	Add(obj){
		;return new this._Binding(this,obj)
		this._Bindings.Insert(obj)
	}
	
	; Request a binding.
	; Returns 1 for OK, you have control of binding system, 0 for no.
	Bind(callback){
		; ToDo: need good way if check if valid callback
		if (this.BindMode || callback = ""){
			return 0
		}
		this._BindModeCallback := callback
		this._DetectBinding()
		return 1
	}
	
	; Converts an Input to a human readable format.
	GetInputHumanReadable(type, code) {
		if (type = HotClass.INPUT_TYPE_K){
			vk := Format("{:x}",code)
			keyname := GetKeyName("vk" vk)
		} else if (type = HotClass.INPUT_TYPE_M){
			keyname := HotClass.MOUSE_BUTTON_NAMES[code]
		}
		StringUpper, keyname, keyname
		return keyname
	}
	
	; Converts a Binding, data pair into a human readable endkey and modifier strings
	GetBindingHumanReadable(binding, data) {
		endkey := this.GetInputHumanReadable(data.type, data.input.vk)	; ToDo: fix for stick?
		if (this.IsWheelType(data)){
			; Mouse wheel cannot be a modifier, as it releases immediately
			if (data.event < 0){
				endkey .= "_U"
			} else {
				endkey .= "_D"
			}
		}
		modifiers := ""
		count := 0
		Loop 2 {
			t := A_Index - 1
			for key, value in binding[t] {
				if (t = data.type && key = data.input.vk){
					; this is the end key - skip
					continue
				}
				if (count){
					modifiers .= " + "
				}
				modifiers .= this.GetInputHumanReadable(t,key)
				count++
			}
		}
		
		return {endkey: endkey, modifiers: modifiers}
	}
	
	; Adds the "common variant" (eg Ctrl) to ONE left/right variant (eg LCtrl) in a State object
	; ScanCode as input
	StateObjAddCommonVariant(obj, state, vk, sc := 0){
		translated_vk := this._MapVirtualKeyEx(sc)
		if ( translated_vk && (translated_vk != vk) ){
			; Has a left / right variant
			obj[HotClass.INPUT_TYPE_K][translated_vk] := state
			return 1
		}
		return 0
	}
	
	; Removes a "Common Variant" (eg Ctrl) from ALL left/right variants (eg Lctrl) in a State object
	; Does not alter the object passed in, returns the new object out.
	StateObjRemoveCommonVariants(obj, data){
		; ToDo: Mouse, stick etc.
		out := {}
		s := ""
		for key, value in obj {
			out[key] := value	; add branch on
			; If this is a left / right version of a key, remove it
			; Convert VK into left / right indistinguishable SC
			res := this._MapVirtualKeyEx(key,0)
			; Convert non left/right sensitve SC back to VK
			res := this._MapVirtualKeyEx(res,1)
			
			if (data.type = HotClass.INPUT_TYPE_K){
				; End key is keyboard - Find "Common" version for end key
				ekc := this._MapVirtualKeyEx(data.input.vk,0)
				ekc := this._MapVirtualKeyEx(ekc,1)
				is_end_key := ( ekc = key  )
			} else {
				is_end_key := 0
			}
			
			; If this has left / right versions, result will be different to the original value, remove it.
			; If this is a common version and also the end key, remove it.
			if (res != key || is_end_key ){
				s .= "removing " key "`n"
				out.Remove(key)
			} else {
				s .= " ignoring " key "`n"
			}
			;tooltip % s
		}
		return out
	}
	
	; Data packet is of mouse wheel motion
	IsWheelType(data){
		return (data.type = HotClass.INPUT_TYPE_M) && (data.input.vk = HotClass.MOUSE_NAME_LOOKUP.Wheel)
	}
	
	; Data packet is an up event for a button or a mouse wheel move (Which does not have up events)
	IsUpEvent(data){
		return ( !data.event || this.IsWheelType(data) )
	}
	
	; INTERNAL / PRIVATE ==========================================================================================================================
	; Anything prefixed with an underscore ( _ ) is not intended for use by end-users.

	; Locks out input and prompts the user to hit the desired hotkey that they wish to bind.
	; Terminates on key up.
	; Returns a copy of the _StateIndex array just before the key release
	_DetectBinding(){
		Gui, New, HwndHwnd -Border
		this._BindPrompt := hwnd
		Gui, % Hwnd ":Add", Text, center w400,Please select what you would like to use for this binding`n`nCurrently, keyboard and mouse input is supported.`n`nHotkey is bound when you release the last key.
		Gui, % Hwnd ":Show", w400
	
		this._BindMode := 1
		return 1
	}

	; Up event or change happened in bind mode.
	; _Stateindex should hold state of desired binding.
	_BindingDetected(data){
		Gui, % this._BindPrompt ":Destroy"
		AsynchBeep(2000)
		
		; Discern End-Key from rest of State
		; "End Pair" (state + endkey data) starts here.
		input_state := {0: this._StateIndex[HotClass.INPUT_TYPE_M], 1: this.StateObjRemoveCommonVariants(this._StateIndex[HotClass.INPUT_TYPE_K], data) }
		output_state := {0: {}, 1: {} }

		; Walk _StateIndex and copy where button is held.
		Loop 2 {
			t := A_Index-1
			s := ""
			for key, value in input_state[t] {
				if ( value && (value != 0) ){
					output_state[t][key] := value
				}
			}
		}
		; call callback, pass _StateIndex structure
		fn := Bind(this._BindModeCallback, output_state, data)
		SetTimer, % fn, -0
		return 1
	}

	; Constructor
	__New(){
		static WH_KEYBOARD_LL := 13, WH_MOUSE_LL := 14
		
		this._StateIndex := []
		this._StateIndex[0] := {}
		this._StateIndex[1] := {0x10: 0, 0x11: 0, 0x12: 0, 0x5D: 0}	; initialize modifier states
		this._StateIndex[2] := {}
		fn := _BindCallback(this._ProcessKHook,"Fast",,this)
		this._hHookKeybd := this._SetWindowsHookEx(WH_KEYBOARD_LL, fn)
		fn := _BindCallback(this._ProcessMHook,"Fast",,this)
		this._hHookMouse := this._SetWindowsHookEx(WH_MOUSE_LL, fn)
		
		;OnMessage(0x00FF, Bind(this._ProcessHID, this))
		;this._HIDRegister()
	}
	
	; Destructor
	__Delete(){
		;this._HIDUnRegister()
	}
	
	; Muster point for processing of incoming input - ALL INPUT SHOULD ULTIMATELY ROUTE THROUGH HERE
	; SetWindowsHookEx (Keyboard, Mouse) to route via here.
	; HID input (eg sticks) to be routed via here too.
	_ProcessInput(data){
		if (data.type = HotClass.INPUT_TYPE_K || data.type = HotClass.INPUT_TYPE_M){
			; Set _StateIndex to reflect state of key
			; lr_variant := data.input.flags & 1	; is this the left (0) or right (1) version of this key?
			if (data.input.vk = 65){
				a := 1	; Breakpoint - done like this so you can hold a modifier but not break.
			}
			if (data.event = 0){
				debug := "Exit Bind Mode Debug Point"
			}
			if ( this._BindMode && this.IsUpEvent(data) ){
				; Key up in Bind Mode - Fire _BindingDetected before updating _StateIndex, so it sees all the keys as down.
				; Pass data so it can see the End Key
				this._BindingDetected(data)
			}
			; Update _StateIndex array
			
			if (data.type = HotClass.INPUT_TYPE_K){
				this.StateObjAddCommonVariant(this._StateIndex, data.event, data.input.vk, data.input.sc)
			}
			this._StateIndex[data.type][data.input.vk] := data.event
			
			; Exit bind Mode here, so we can be sure all input generated during Bind Mode is blocked, where possible.
			; ToDo data.event will not suffice for sticks?
			if ( this._BindMode && this.IsUpEvent(data) ){
				if (this.IsWheelType(data) && data.input.vk = HotClass.MOUSE_NAME_LOOKUP.Wheel){
					; Mouse Wheel has no up event, so release it now
					this._StateIndex[data.type][data.input.vk] := 0
				}
				this._BindMode := 0
			}
			
			; Do not process any further in Bind Mode
			if (this._BindMode){
				return 1
			}

			; find the total number of modifier keys currently held
			modsheld := this._StateIndex[data.type][0x10] + this._StateIndex[data.type][0x11] + this._StateIndex[data.type][0x5D] + this._StateIndex[data.type][0x12]
			
			; Find best match for binding
			best_match := {binding: 0, modcount: 0}
			Loop % this._Bindings.MaxIndex() {
				b := A_Index
				if (this._Bindings[b].type = data.type && this._Bindings[b].input = data.input.vk && this._Bindings[b].event = data.event){
					max := this._Bindings[b].modifiers.MaxIndex()
					if (!max){	; convert "" to 0
						max := 0
					}
					matched := 0

					if (!ObjHasKey(this._Bindings[b].modifiers[1], "type")){
						; If modifier array empty, match
						max := 0
						best_match.binding := b
						best_match.modcount := 0
					} else {
						Loop % max {
							m := A_Index
							if (this._StateIndex[this._Bindings[b].modifiers[m].type][this._Bindings[b].modifiers[m].input]){
								; Match on one modifier
								matched++
							}
						}
					}
					if (matched = max){
						; All modifiers matched - we have a candidate
						if (best_match.modcount < max){
							; If wild not set, check no other modifiers in addition to matched ones are set.
							if ((modsheld = max) || this._Bindings[b].modes.wild = 1){
								; No best match so far, or there is a match but it uses less modifiers - this is current best match
								best_match.binding := b
								best_match.modcount := max
							}
						}
					}
				}
			}
			
			; Decide whether to fire callback
			if (best_match.binding){
				; A match was found, call
				fn := this._Bindings[best_match.binding].callback
				; Start thread for bound func
				SetTimer %fn%, -0
				; Block if needed.
				if (this._Bindings[best_match.binding].modes.passthru = 0){
					; Block
					return 1
				}
			}
		}
		return 0
	}

	; Process Keyboard messages from Hooks and feed _ProcessInput
	_ProcessKHook(nCode, wParam, lParam){
		; KBDLLHOOKSTRUCT structure: https://msdn.microsoft.com/en-us/library/windows/desktop/ms644967%28v=vs.85%29.aspx
		Critical
		
		If ((wParam = 0x100) || (wParam = 0x101)) { ; WM_KEYDOWN || WM_KEYUP
			lp := new _Struct(WinStructs.KBDLLHOOKSTRUCT,lParam+0)
			if (this._ProcessInput({type: HotClass.INPUT_TYPE_K, input: { vk: lp.vkCode, sc: lp.scanCode, flags: lp.flags}, event: wParam = 0x100})){
				; Return 1 to block this input
				; ToDo: call _ProcessInput via another thread? We only have 300ms to return 1 else it wont get blocked?
				return 1
			}
		}
		Return this._CallNextHookEx(nCode, wParam, lParam)
	}
	
	; Process Mouse messages from Hooks and feed _ProcessInput
	_ProcessMHook(nCode, wParam, lParam){
		; MSLLHOOKSTRUCT structure: https://msdn.microsoft.com/en-us/library/windows/desktop/ms644970(v=vs.85).aspx
		static WM_LBUTTONDOWN := 0x0201, WM_LBUTTONUP := 0x0202 , WM_RBUTTONDOWN := 0x0204, WM_RBUTTONUP := 0x0205, WM_MBUTTONDOWN := 0x0207, WM_MBUTTONUP := 0x0208, WM_MOUSEHWHEEL := x020E, WM_MOUSEWHEEL := 0x020A, WM_XBUTTONDOWN := 0x020B, WM_XBUTTONUP := 0x020C
		Critical
		
		; Filter out mouse move and other unwanted messages
		If ( wParam = WM_LBUTTONDOWN || wParam = WM_LBUTTONUP || wParam = WM_RBUTTONDOWN || wParam = WM_RBUTTONUP || wParam = WM_MBUTTONDOWN || wParam = WM_MBUTTONUP || wParam = WM_MOUSEWHEEL || wParam = WM_MOUSEHWHEEL || wParam = WM_XBUTTONDOWN || wParam = WM_XBUTTONUP ) {
			lp := new _Struct(WinStructs.MSLLHOOKSTRUCT,lParam)
			if (wParam = WM_MOUSEWHEEL || wParam = WM_MOUSEHWHEEL){
				mouseData := new _Struct("Short sht",lp.mouseData_high[""]).sht
			} else {
				mouseData := lp.mouseData_high
			}
			;ToolTip % "md: " mouseData
			
			flags := lp.flags
			
			vk := HotClass.MOUSE_WPARAM_LOOKUP[wParam]
			if (wParam = WM_LBUTTONUP || wParam = WM_RBUTTONUP || wParam = WM_MBUTTONUP ){
				; Normally supported up event
				event := 0
			} else if (wParam = WM_MOUSEWHEEL || wParam = WM_MOUSEHWHEEL) {
				; Mouse wheel has no up event
				vk := HotClass.MOUSE_WPARAM_LOOKUP[wParam]
				; event = 1 for up, -1 for down
				if (mouseData < 0){
					event := 1
				} else {
					event := -1
				}
			} else if (wParam = WM_XBUTTONDOWN || wParam = WM_XBUTTONUP ){
				if (wParam = WM_XBUTTONUP){
					debug := "me"
				}
				vk := 3 + mouseData
				event := (wParam = WM_XBUTTONDOWN)
			} else {
				; Only down left
				event := 1
			}
			;tooltip % "type: " HotClass.INPUT_TYPES[HotClass.INPUT_TYPE_M] "`ncode: " vk "`nevent: " event
			if (this._ProcessInput({type: HotClass.INPUT_TYPE_M, input: { vk: vk}, event: event})){
				; Return 1 to block this input
				; ToDo: call _ProcessInput via another thread? We only have 300ms to return 1 else it wont get blocked?
				return 1
			}
		} else if (wParam != 0x200){
			debug := "here"
		}
		Return this._CallNextHookEx(nCode, wParam, lParam)
	}
	
	_SetWindowsHookEx(idHook, pfn){
		Return DllCall("SetWindowsHookEx", "int", idHook, "Uint", pfn, "Uint", DllCall("GetModuleHandle", "Uint", 0, "Ptr"), "Uint", 0, "Ptr")
	}

	_UnhookWindowsHookEx(hHook){
		Return DllCall("UnhookWindowsHookEx", "Uint", hHook)
	}

	_CallNextHookEx(nCode, wParam, lParam, hHook = 0){
		Return DllCall("CallNextHookEx", "Uint", hHook, "int", nCode, "Uint", wParam, "Uint", lParam)
	}

	; https://msdn.microsoft.com/en-us/library/windows/desktop/ms646307(v=vs.85).aspx
	; scan code is translated into a virtual-key code that does not distinguish between left- and right-hand keys
	_MapVirtualKeyEx(nCode, uMapType := 1){ ; MAPVK_VSC_TO_VK
		; Get locale
		static dwhkl := DllCall("GetKeyboardLayout", "uint", 0)
		
		ret := 0
		; MAPVK_VSC_TO_VK - The uCode parameter is a scan code and is translated into a virtual-key code
		; Check cache
		if (!this[this._MapTypes[uMapType]][nCode]){
			; Populate cache
			ret := DllCall("MapVirtualKeyEx", "Uint", nCode, "Uint", uMapType, "Ptr", dwhkl, "Uint")
			if (ret = ""){
				ret := 0
			}
			this[this._MapTypes[uMapType]][nCode] := ret
		} else {
			; cache hit
			ret := this[this._MapTypes[uMapType]][nCode]
		}
		; Return result
		return ret
	}
}

; bind by Lexikos
; Requires test build of AHK? Will soon become part of AHK
; See http://ahkscript.org/boards/viewtopic.php?f=24&t=5802
bind(fn, args*) {  ; bind v1.2
    try bound := fn.bind(args*)  ; Func.Bind() not yet implemented.
    return bound ? bound : new BoundFunc(fn, args*)
}

class BoundFunc {
    __New(fn, args*) {
        this.fn := IsObject(fn) ? fn : Func(fn)
        this.args := args
    }
    __Call(callee, args*) {
        if (callee = "" || callee = "call" || IsObject(callee)) {  ; IsObject allows use as a method.
            fn := this.fn, args.Insert(1, this.args*)
            return %fn%(args*)
        }
    }
}

; _BindCallback by GeekDude
_BindCallback(Params*)
{
    if IsObject(Params)
    {
        this := {}
        this.Function := Params[1]
        this.Options := Params[2]
        this.ParamCount := Params[3]
        Params.Remove(1, 3)
        this.Params := Params
        if (this.ParamCount == "")
            this.ParamCount := IsFunc(this.Function)-1 - Floor(Params.MaxIndex())
        return RegisterCallback(A_ThisFunc, this.Options, this.ParamCount, Object(this))
    }
    else
    {
        this := Object(A_EventInfo)
        MyParams := [this.Params*]
        Loop, % this.ParamCount
            MyParams.Insert(NumGet(Params+0, (A_Index-1)*A_PtrSize))
        return this.Function.(MyParams*)
    }
}
Last edited by evilC on 14 Feb 2015, 12:36, edited 18 times in total.
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: [WIP] Hotkeys done via SetWindowsHookEx Calls

25 Jan 2015, 19:09

Known Issues:
  • Win key does not work as a modifier, only as an End Key !?
  • My attempts to work around the fact that the mouse wheel has no up event have so far failed. One you use the mouse wheel, the system will think it is "down" until you restart the script.
Current stage of development

At the moment, you can add bindings, but not remove them.
There is also no test code to try routing the output of a test of the bind routine to the hotkey add system.

I am still considering architecture options etc, so the future of this library remains undecided.
I currently have a lot of other projects on the boil, so this one may well take a back burner.
Last edited by evilC on 14 Feb 2015, 12:42, edited 3 times in total.
User avatar
hoppfrosch
Posts: 443
Joined: 07 Oct 2013, 04:05
Location: Rhine-Maine-Area, Hesse, Germany
Contact:

Re: [WIP] Hotkeys done via SetWindowsHookEx Calls

26 Jan 2015, 08:51

Hmmmm ... trying to run your (unmodified) code on AutoHotkey 1.1.19.02 32bit Unicode.

When pressing hotkey "CTRL-a" an error pops up:

Code: Select all

 ---------------------------
Script.ahk
---------------------------
Error:  Target label does not exist.

Specifically: HighBeep

	Line#
	133: }
	134: }
	135: }
	136: }
	138: if (best_match.binding)  
	138: {
	140: fn := this._Bindings[best_match.binding].callback
--->	142: SetTimer,%fn%,-0
	144: if (this._Bindings[best_match.binding].modes.passthru = 0)  
	144: {
	146: Return,1
	147: }
	148: }
	149: }
	150: Return,0

The current thread will exit.
Pressing "CTRL-SHIFT-a" the same error - but on LowBeep() ....
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: [WIP] Hotkeys done via SetWindowsHookEx Calls

26 Jan 2015, 09:08

You may well need the test build of AHK. I thought GeekDude tested it and it worked on vanilla AHK, but maybe something has changed since that requires the test build.

You want it anyway, it's awesome (both the AHK version and the Bind() function in the thread)

http://ahkscript.org/boards/viewtopic.php?f=24&t=5802
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: [WIP] Hotkeys done via SetWindowsHookEx Calls

27 Jan 2015, 01:37

Code in OP updated.

Mouse support is now in, along with a pretty much working Bindings system.

Binding demo is now kicked off on launch.

Note:
Currently, the "End Key" (ie the last key you hit for a binding - the key you release to exit bind mode) is a separate entity to the other keys that were held at the point of the bind (the "modifiers"). this is by design.

Holding A and hitting B (endkey: B, modifier: A) is different to holding B and hitting A (endkey: A, modifier: B).

In theory, any crazy combination should be possible - eg
hold LCTRL + Left Mouse + Joystick 2 button 3, hit LShift = CTRL+LMouse+J2B3+LShift (Notice how Ctrl is a left/right agnostic requirement, but LShift isn't as it is an Endkey).

Some examples of the combos that it currently recognizes (Actual output from the demo bind):

Code: Select all

; Modifier-only hotkeys, with smart left/right variant filtering
You Hit LSHIFT while holding LBUTTON + CONTROL
; Basic binding
You Hit B while holding SHIFT + CONTROL + ALT
; End Key is important !
You Hit A while holding B + C
You Hit B while holding A + C
You Hit C while holding A + B
; OK, let's ramp it up
You Hit XBUTTON1 while holding LBUTTON + RBUTTON + SHIFT + CONTROL + ALT + A + Z
; Err, use Logitech G13 with left hand, keyboard with right? WINNING! TEN KEY COMBO !!!
You Hit S while holding SHIFT + CONTROL + ALT + 1 + 2 + A + E + Q + W
Known Bugs:
WIN does not work as a modifier (though it does work as an end key ?!)

(FYI it pastes to the clipboard on bind, so use that for bug reporting etc if you need)
User avatar
submeg
Posts: 326
Joined: 14 Apr 2017, 20:39
Contact:

Re: [WIP] HotClass - Dynamic Hotkey and Binding system

12 Feb 2021, 02:38

hey @evilC, this is very intriguing! Where did this end up going in the end?
____________________________________
Check out my site, submeg.com
Connect with me on LinkedIn
Courses on AutoHotkey :ugeek:
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: [WIP] HotClass - Dynamic Hotkey and Binding system

12 Feb 2021, 10:09

I never really ended up pursuing it more - If memory serves I originally wrote this for potential use with UCR
The AHK version of UCR has now been deprecated in favour of a C# version, however I distilled down the dynamic hotkeys system that I used for UCR into the AppFactory library.
I also now have AutoHotInterception which provides driver-level blocking, multi-device support (ie each keyboard and mouse is separately remappable and blockable), along with RawInput-like mouse movement support

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: Theda and 151 guests