[Class] LongHotkeys (a & b & c & ...)

Post your working scripts, libraries and tools
Helgef
Posts: 3569
Joined: 17 Jul 2016, 01:02
Contact:

[Class] LongHotkeys (a & b & c & ...)

22 Oct 2016, 06:32


Introduction.

Edit 2017-12-05: also on :arrow: github.
The purpose of this class is to enable easy usage of custom multi-key hotkeys, using a familiar syntax, that is, to resemble the built-in Custom combination hotkeys, but to allow any number of ampersands (&) and keys to be combined into a hotkey.
The following example demonstrates the main usage of this class,

Code: Select all

#Include longhotkey.ahk
new LongHotkey("1 & 2 & 3 & 4", "myFunc","Hello LongHotkey")

myFunc(str){
        MsgBox, % str
}
To trigger the long hotkey, and hence call myFunc("Hello LongHotkey"), simply press 1+2+3+4.


Download

Copy the code in the codebox, and save as longhotkey.ahk

Code: Select all

class LongHotkey
{
	;	;	;	; 	; 	;	;	;	;
	; Author: Helgef
	; Date: 2016-10-22
	; Instructions: https://autohotkey.com/boards/viewtopic.php?f=6&t=24145
	;
	
	; Class variables
	static instanceArray:=Object()										; Holds references to all objects derived from this class.
	static allHotkeys:=Object()											; Holds all registred hotkeys.
	static doNothingFunc:=ObjBindMethod(LongHotkey,"doNothing")			; All hotkeys are bound to this function.
	static globalContextFunc:=""										; Global context function, set via setGlobalContext()
	static allSuspended:=0												; Used for pseudo-suspending all hotkeys.
	static PriorLhk:=""													; Stores the long hotkey that was completed prior to the most recent completed lhk.
	static mostRecentLhk:=""											; Stores the most recent lhk.
	static TimeOfPriorLongHotkey:=""									; Stores the A_TickCount for the PriorLongHotkey.
	static TimeOfMostRecentLongHotkey:=""								; Stores the A_TickCount for the most recently completed LongHotkey.
	static hasFirstUps:=0												; Keeps track of whether any lhk has specified FirstUp option.
	static LatestFirstUp:=""											; For the first up option.
	; Instance variables
	hits:=0																; Tracks the progress of the long hotkey.
	contextFunc:=""														; Specified through setContext() method, to make the long hotkey context sensitive.
	suspended:=0														; Set by suspend() method, for pseudo-suspending hotkey.
	FirstUpAllowed:=1													; Used when enableFirstUp(true) has been called, to determine if upFunc should be called.
	upFunc:=""															; See comment on FirstUpAllowed.
	TimeOfThisLongHotkey:=""											; Stores the time of  the last completion for this lhk.
	
	;
	; Callable instance methods
	;
	
	setContext(function:="",params*)
	{
		; Provide a function with an optional number of parameters to act as context function.
		; This function should return true if the hotkey should be considered as in context, else false.
		; Call setContext() without passing any parameters to remove context.
		if IsFunc(function)
			this.contextFunc:=Func(function).Bind(params*)
		else if (function="")
			this.contextFunc:=""
		return
	}
	
	suspend(bool:=1)
	{
		; Pseudo-supend this hotkey. The registred hotkeys will remain, but the evaluation will terminate quick and no triggering is possible.
		; Call with bool:=1 or do not pass a parameter at all, to invoke suspension.
		; Call with bool:=0 to cancel suspension.
		; Call with bool:=-1 to toggle suspension.
		; Returns the current suspension state, ie., 1 for being suspended, 0 for not suspended.
		return this.suspended:=bool=-1?!this.suspended:bool
	}
	
	setFunction(function,params*)
	{
		; Specify the name of the function that will be called when the long hotkey is completed, along with any number of parameters.
		; Can be a label.
		; If 'function' is not a function nor label, it is considered to be a string to send, it will be passed to LongHotkey.send()
		; By default a reference to this hotkey is pushed into the params* array.
		if	RegExMatch(function,"@$")															; Mark the function name with an @ (at) at the end to omit the reference to this instance in the params* array.	Eg, "myFunc@"
			function:=SubStr(function,1,-1)
		else		
			params.push(this)
			
		if IsFunc(function)
			this.function:=Func(function).Bind(params*)											; Function to call when sequence is completed, with params.
		else if IsLabel(function)
			this.function:=function																; Label to call when sequence is completed.
		else if (function!="")																	
			this.function:=ObjBindMethod(LongHotkey,"Send",function)							; The parameter 'function' was not function nor label, send it as string instead.
		else
			this.function:=""	
	}
	
	enableFirstUp(enable:=1)
	{
		; Enable the first key in the long hotkey to trigger its normal press event on its release, in the case when no other keys in the long hotkey was pressed.
		; Call this function with parameter 0 to disable this behaviour.
		if enable
		{
			LongHotkey.hasFirstUps:=this.upFunc?LongHotkey.hasFirstUps:++LongHotkey.hasFirstUps ; Only increment hasFirstUps counter if upFunc doesn't exist.
			_downKey:=RegExReplace(this.keyList[1].down,"(?:\*|)(\w+)","{$1}") ; Encloses key name in "{ }"
			this.upFunc:=ObjBindMethod(LongHotkey,"SendFirstUp",_downKey)
		}
		else if (this.UpFunc!="")	; This check is to avoid decrement LongHotkey.hasFirstUps unless it has an upFunc
		{
			this.upFunc:=""
			LongHotkey.hasFirstUps--
		}
		return
	}
	
	ThisLongHotkey()
	{
		; Similar to A_ThisHotkey.
		; Call this method on the reference passed to the success function, to get back the string that defined the hotkey.
		; Eg, A_ThisLongHotkey:=lh.ThisLongHotkey(), where lh is the last parameter of the success function, eg, f(x1,...,xn,lh)
		return this.keys
	}
	
	TimeSinceThisLongHotkey()
	{
		; Similar to A_TimeSinceThisHotkey.
		; Returns the time (in ms) since this lhk was triggered.
		; If the long hotkey has never been triggered, this method returns -1.
		return this.TimeOfThisLongHotkey?A_TickCount-this.TimeOfThisLongHotkey:-1
	}
	
	getKeyList()
	{
		; Similar to the ThisLongHotkey() method, but here an array is returned. Note that modifers doesn't get their own spots in the array, eg,
		; keys:="^ & a & b ! & c" transforms to keyList:=[^a, ^b, ^!c]
		; This description is not correct any more                  																		< - - NOTE
		return this.keyList
	}
	
	unregister()
	{
		; Unregister this long hotkey. To free the object, do lh:="" afterwards, if you wish. Here, lh is an instance of the class LongHotkey.
		; To reregister, use reregister() method (not if lh:="" was done, obviously).
		_clonedList:=this.keyList.clone()
		LongHotkey.instanceArray.delete(this.instanceNumber)
		Hotkey, If, LongHotkey.Press()
		For _k, _key in _clonedList  ; For each key(.down) in this long hotkey, check if it is registred in any of the other hotkeys, if it is, do not unregister it, else, do.
		{
			_deleteThis:=1
			For _l, _lh in LongHotkey.instanceArray						; Search for the key in another long hotkey, if it is found, do not delete it.
			{
				For _m, _dndKey in _lh.keyList				
				{
					if (_key.down=_dndKey.down)	
					{				
						_deleteThis:=0 ; do not delete key.
						break,2
					}
				}
			}
			if _deleteThis
			{
				Hotkey, % _key.down, Off									; Turn off the hotkey, and remove it from the allHotkeys list.
				LongHotkey.allHotkeys.delete(_key.down)
			}
		}
		this.keyList:=_clonedList									    ; The clone lives.
		this.registred:=0												; This long hotkey is not registred any more.
		Hotkey, If
		return 
	}
	
	reregister()
	{
		; Reregister a long hotkey, return 1 on success, 0 otherwise.
		if this.registred
			return 0
		For _k, _key in this.keyList 										; If we get here, this is the clone.
			LongHotkey.RegisterHotkey(_key.down)
		this.instanceNumber:=LongHotkey.instanceArray.push(this)
		this.registred:=1
		return 1
	}
	
	;
	;	Callable class methods
	;
	
	suspendAll(bool:=1)
	{
		; Pseudo-supend all long hotkeys.
		; The registred hotkeys will remain, but the evaluation will terminate quick and no triggering is possible.
		; To truly suspend, use the built in Supend command.
		; Call with bool:=1 or do not pass a parameter at all, to invoke suspension.
		; Call with bool:=0 to cancel suspension.
		; Call with bool:=-1 to toggle suspension.
		; Returns the current suspension state, ie., 1 for all is suspended, 0 for not suspended
		return LongHotkey.allSuspended:=bool=-1?!LongHotkey.allSuspended:bool
	}
	
	setGlobalContext(function:="",params*)
	{
		; Provide a function with an optional number of parameters to act as global context function.
		; If this function is set and returns 0 no hotkey is active.
		; This function should return true if the hotkey should be considered as in context, else false.
		; Call with setGlobalContext() without any parameters to remove context.
		if IsFunc(function)
			LongHotkey.globalContextFunc:=Func(function).Bind(params*)
		else if (function="")
			LongHotkey.globalContextFunc:=""
		return
	}	
	unregisterAll(onOff:="Off")
	{
		; Unregisters all hotkeys.
		; Do not pass a parameter
		Hotkey, If, LongHotkey.Press()
		For _key in LongHotkey.allHotkeys
			Hotkey, % _key, % onOff
		Hotkey, If,
		return
	}
	
	reregisterAll()
	{
		; Reregisters all hotkeys.
		return LongHotkey.unregisterAll("On")
	}
	
	MostRecentLongHotkey(ref:=0)
	{
		; This is returns the most recently completed long hotkey.
		; Call with ref:=0 or without any parameter, to get the key string that defined the hotkey that triggered most recently, eg,
		; "a & b & c".
		; Call with ref:=1 to recieve a reference to the most recent long hotkey instead.
		return !ref?LongHotkey.MostRecentLhk.keys:LongHotkey.MostRecentLhk
	}
	
	TimeSinceMostRecentLongHotkey()
	{
		; Returns the time since (in ms.) the the most recently lhk was triggered.
		; If no long hotkey has been triggered, this method returns -1.
		return LongHotkey.TimeOfMostRecentLongHotkey?A_TickCount-LongHotkey.TimeOfMostRecentLongHotkey:-1
	}
	
	PriorLongHotkey(ref:=0)
	{
		; Similar to A_PriorHotkey.
		; Call with ref:=0 or without any parameter, to get the key string that defined the hotkey that triggered prior to the most recent one, eg,
		; "a & b & c".
		; Call with ref:=1 to recieve a reference to the prior long hotkey instead.
		; returns blank if no PriorLongHotkey exists.
		return !ref?LongHotkey.Priorlhk.keys:LongHotkey.Priorlhk
	}
	
	TimeSincePriorLongHotkey()
	{
		; Similar to A_TimeSincePriorHotkey
		; Returns the time since (in ms.) the prior lhk was last triggered. That is, time since the lhk prior to the most recent one was triggered.
		; If there is no prior long hotkey, this method returns -1.
		return LongHotkey.TimeOfPriorLongHotkey?A_TickCount-LongHotkey.TimeOfPriorLongHotkey:-1
	}
	;
	; End callable methods.
	;
	
	__New(keys,function,params*)
	{
		this.instanceNumber:=LongHotkey.instanceArray.Push(this)	
		this.length:=this.processKeys(keys)							; Creates a "keyList" for this instance, and returns the appropriate length.
		this.registred:=1											; Indicates that the hotkeys are registred.
		this.keys:=keys
		this.setFunction(function,params*)
	}
	
	processKeys(str)
	{
		; Pre-process routine, runs once per new long hotkey.
		; Converts the key string (str) to an array, keyList[n]:={down:modifier key_n, up: "*key_n up", tilde:true/false}
		; Eg, "^ & ~a & b & ! & c" -> 	keyList[1]:={down: "^a",  up: "*a up", tilde: 1}
		;								keyList[2]:={down: "^b",  up: "*b up", tilde: 0}
		;								keyList[3]:={down: "^!c", up: "*c up", tilde: 0}
		; Also makes a slightly redunant array: keyUpList[keyList[n].up]:=n. It is used in the Release() function, to quickly determine which part of the hotkey sequnce was released.
		;
		
		this.keyList:=Object()
		this.keyUpList:=Object()
		_modifiers:=""
		; Adjust key string (str) to fit pre-process routine
		str:=RegExReplace(str,"\s","") 									; Remove spaces.
		; Transfrom modifiers given by name to symbol styled modifier, eg, "LCtrl" --> "<^"
		_ModifierList := { LControl:"<^",LCtrl:"<^",RControl:">^",RCtrl:">^",Ctrl:"^",Control:"^"
						,LAlt:"<!",RAlt:">!",Alt:"!"
						,LShift:"<+",RShift:">+",Shift:"+"
						,LWin:"<#",RWin:">#",Win:"#"					; "Win" is not a supported key, but it works here.
						,AltGr:"<^>!"}
		
		; prepend @0 to last key if it is a modifier. Cheap way to make modifiers work as last key. This is an after-constructionm, due to some oversight. 
		if _ModifierList.HasKey(RegExReplace(str,".*&(.*)","$1"))
			str:=RegExReplace(str,"(.*&)","$1@0")
		For _name, _symbol in _ModifierList
			str:=RegExReplace(str,"i)\b" _name "\b", _symbol)			; Swap names for symbols.
		; Parse 1, tilde ~
		_ctr:=0															; For correct indexing of tilde ~.
		Loop, Parse, str,&
		{
			if RegExMatch(A_LoopField,"[\^!#+]+")
				continue
			_ctr++
			this.keyList[_ctr]:={}										; Create an empty "sub-object" at index _ctr, this will have three key-value pairs: down,up,tilde.
			if RegExMatch(A_LoopField,"~")
				this.keyList[_ctr].tilde:=1								; If key has tilde, 0 should be returned from Press(), then the key is not suppressed.
			else
				this.keyList[_ctr].tilde:=0								; Keys without tilde, should be suppressed.
		}
		str:=RegExReplace(str,"~","") 									; Remove all ~
		; Parse 2, set up key list and register hotkeys.
		_ctr:=0															; For correct indexing.
		Loop, Parse, str,&
		{
			if RegExMatch(A_LoopField,"[\^!#+]+") && !InStr(A_LoopField, "@0")						; Check if modifers. @0 is to allow last key as modifier.
			{
				_modifiers:=LongHotkey.sortModifiers(_modifiers A_LoopField)
				continue
			}
			_ctr++
			_key:=RegExReplace(A_LoopField,"@0")				; This is a cheap way to make modifiers work as last key
			LongHotkey.RegisterHotkey(_modifiers _key)			; Register this hotkey, i.e, modifier+key.
			this.keyList[_ctr].down:=_modifiers _key 			; Down events will trigger Press()					
			this.keyList[_ctr].up:=(InStr(_key,"*")?"":"*") _key " up" 	; Using this format for the up event is due to that there seemed to be problems with modifiers. That is good english.					
			; Test
			this.keyUpList[this.keyList[_ctr].up]:=_ctr					; This is slightly redundant, but it should improve performance.
			LongHotkey.allHotkeys[_modifiers _key]:=""			; This is used for un/re-registerAll().
		}
		return _ctr	; Return the length of the sequence.
	}
	
	sortModifiers(unsorted)
	{
		; Helper function for process keys. Sorts modifiers, to aviod, eg, ^!b and !^b, this enables user to instanciate
		; long hotkeys like, "ctrl & a & alt & b" and "alt & a & ctrl & b", simultaneously.
		_ModifierList := [{"<^>!":4},{"<^":2},{">^":2},{"<!":2},{">!":2},{"<+":2}
						 ,{">+":2},{"<#":2},{">#":2},{"+":1},{"^":1},{"!":1},{"#":1}]
		_sorted:=""
		For _k, _mod in _ModifierList				; This is 13 iterations.
			For _symbol, _len in _mod				; This loop is one iteration.
				if (_p:=InStr(unsorted,_symbol))
					_sorted.=SubStr(unsorted,_p,_len), unsorted:=StrReplace(unsorted,_symbol,"")
		
		return _sorted
	}
	
	;;
	;;	Hotkey evaluation methods.
	;;
	
	Press()
	{
		Critical,On
		if (LongHotkey.allSuspended || (LongHotkey.LatestFirstUp!="" && ((LongHotkey.LatestFirstUp:="") || 1)))	; If pseudo-suspended, return 0 immediately, or if first up is needed to be suppressed.
			return 0
		if (LongHotkey.globalContextFunc!="" && !LongHotkey.globalContextFunc.Call())			; Global context check
			return 0
		_upEventRegistred:=0																	; To aviod registring the up event more than once
		_oneHit:=0, _tilde:=0																	; These values will togheter determine if the output should be suppressed or not, it is an unfortunate solution, w.r.t. maintainabillity. Hopefully it works and need not be changed.
		_priority:=0, _dp:=0	; In case a lot of hotkeys are triggered at the same time, or any other reason, one can set dp:=1 to make sure the timers for the success functions is set with decreaseing priority, hence they will not interupt eachother, but execute in the order their settimer is created. If you want the timers not to be interupted by other timers and such, set priority to a high enough value.
		For _k, _lh in LongHotkey.instanceArray
		{
			if (_lh.hits=0 && (_lh.suspended || (_lh.contextFunc!="" && !_lh.contextFunc.Call()))) 	; Check if suspended, and check context only when first key is pressed.
				continue
			if (_lh.hits>0 && _lh.keyList[_lh.hits].down=A_ThisHotkey)							; Key is same as last, suppress and continue.
			{
				_oneHit:=1
				_tilde+=_lh.keyList[_lh.hits].tilde
				continue
			}
			if (_lh.keyList[_lh.hits+1].down=A_ThisHotkey)										; Check if advanced.
			{				
				_oneHit:=1
				_lh.hits+=1																		; Update hit count.
				_tilde+=_lh.keyList[_lh.hits].tilde
				if (_lh.hits=_lh.length)														; Hotkey completed.
				{
					_timerFunction:=_lh.function												; Set up function call.
					SetTimer, % _timerFunction,-1,% _priority - _dp								; priority and dp is explaind a few lines up.			
					_lh.hits-=1																	; Decrement the hit count to enable auto-repeat of the hotkey-
					_lh.FirstUpAllowed:=0														; No "first up" if hotkey completed.
					; Manage TimeSince, and PriorLongHotkey stuff.
					_lh.TimeOfThisLongHotkey:=A_TickCount
					LongHotkey.PriorLhk:=LongHotkey.mostRecentLhk								; Manage prior long hotkey.
					LongHotkey.mostRecentLhk:=_lh
					LongHotkey.TimeOfPriorLongHotkey:=LongHotkey.TimeOfMostRecentLongHotkey		; Time stamps.
					LongHotkey.TimeOfMostRecentLongHotkey:=A_TickCount
					continue 																	; No need to bind up-event for last key.
				}
				if !_upEventRegistred
				{
					_doNothingFunc:=LongHotkey.doNothingFunc									; Hotkey has advanced, but not compeleted.
					Hotkey, If, LongHotkey.Release()											; Bind up-event.
					Hotkey, % _lh.keyList[_lh.hits].up, % _doNothingFunc, On
					Hotkey, If,
					_upEventRegistred:=1
				}
			}
		}
		return _oneHit*(_tilde=0)  ; If there is no hit, no suppress, if there is a tilde but no hotkey advanced/completed, no suppress.
	}
	
	Release()
	{
		; Every time a key is released, all long hotkeys are set to zero hits.
		Critical, On
		Hotkey, If, LongHotkey.Release()			; Unbind this up-event.
		Hotkey, % A_ThisHotkey, Off
		Hotkey, If
		; Determine if this up event should reset or decrease the hit count for any long hotkey. Also, manages the "first_up" option
		_oneTimerSet:=0												; For first up option
		if LongHotkey.hasFirstUps									; Do this check to avoid calling noMultiHits() unless necessary.
			_noMultiHits:=LongHotkey.noMultiHits() 					; For first up option
		
		For _k, _lh in LongHotkey.instanceArray
		{
			if (_lh.hits=0)
				continue
			if (_noMultiHits && !_oneTimerSet && _lh.upFunc!="" && _lh.hits=1 && _lh.FirstUpAllowed && _lh.keyList[1].up=A_ThisHotkey)
			{
				_timerFunction:=_lh.upFunc
				SetTimer,% _timerFunction,-1
				_oneTimerSet:=1										; Only send one time, in case more than one hotkey has this as first key.
				LongHotkey.LatestFirstUp:=_lh.keyList[1].down		; This is needed to disable Press() when the first up is triggered.
			}
			; Determine new hit count for this long hotkey.
			_n:=_lh.keyUpList[A_ThisHotkey]
			if (_n!="" && _n<=_lh.hits)
				_lh.hits:=_n-1
			_lh.FirstUpAllowed:=_lh.hits?_lh.FirstUpAllowed:1
		}
		return 0
	}
	
	noMultiHits()
	{
		; Helper function for FirstUp option, called from Release()
		For _k, _lh in LongHotkey.instanceArray
			if !_lh.FirstUpAllowed
				return 0
		return 1
	}
	
	RegisterHotkey(key)
	{
		; Register key to function doNothing(), under context LongHotkey.Press()
		_doNothingFunc:=LongHotkey.doNothingFunc
		Hotkey, If, LongHotkey.Press()
		if !LongHotkey.allHotkeys.HasKey(key)	; Make sure key not already registred.
		{
			Hotkey,% key,% _doNothingFunc, On
			LongHotkey.allHotkeys[key]:=""
		}
		Hotkey, If
		return
	}
	
	doNothing(){
		return		; All hotkeys are bound to this, serves two purposes:
	}				; 1. The hotkey command require a function/label, 2. calling this function will suppress the "usual" output of the hotkey, when needed.
	
	SendFirstUp(key)
	{
		; Function to send first key in case enableFirstUp(1) has been called
		SendLevel,1 ; If there is problems with the first up function, try to increase/remove this. 
		Send, % key
		return
	}
	
	Send(str)
	{
		; Mostly for testing, but works if wanted.
		SendInput, % str
		return
	}
	
	; For the hotkey command.
	#If LongHotkey.Press()
	#If LongHotkey.Release()
	#If
}


General instructions.

The following two steps are needed.

(1) Include the class:

Code: Select all

; Top of the script
#include longhotkey.ahk
(2) Create a new long hotkey:

Code: Select all

LhObj := new LongHotkey(Keys, Function [, Params*])
Parameters
Keys
An &-delimited string of keys, eg, Keys:="LCtrl & 5 & <^>! & LButton & F1 & SC029 & a". You may also put a * or ~ in front of the key name. See the Behaviour and design section for more details.
Keys that do not have an up-event, eg WheelDown, can only be used last in a sequence.
Function
Can be the name of any built-in or custom function or a label, that will be called when the hotkey sequence is completed, it can also be a string, in which case it will be sent as text, eg, new LongHotkey("...", "Some text to send..."). Note: Functions are called after all long hotkeys have been evaluated, and they are called as SetTimer, % func, -1.
Params*
Can be any number of parameters that should be passed to the function. Eg, new LongHotkey("key1 & ... & keyN", "funcName", param1,...,paramM). A reference to the long hotkey is automatically passed to the function at param(M+1), avoid this by appending an @ to the function name. Eg, myFunc@.
You do not need to read any further to use this class, however, if you are interested in some additional features and limitations, a description of the hotkeys behaviour and the implementation, please continue.

Instance methods.

You do not need to keep a reference, eg, myLh:=..., to the instance if you do not wish. However, the references are needed if you wish to use any of the following instance methods.

Code: Select all

setContext(function:="",params*)
; Provide a function with an optional number of parameters to act as context function.
; This function should return true if the hotkey should be considered as in context, else false.
; Call setContext() without passing any parameters to remove context.

suspend(bool:=1)
; Pseudo-suspends a long hotkey
; Input, bool:=
;		1 to suspend
;		0 to unsuspend
;		-1 to toggle suspension state

setFunction(function,params*)
; Specify the name of the function that will be called when the long hotkey is completed, along with any number of parameters.
; Can be a label.
; If 'function' is not a function nor label, it is considered to be a string to send, it will be passed to LongHotkey.send()
; By default a reference to this hotkey is pushed into the params* array.
; Mark the function name with an @ (at) at the end to omit the reference to this instance in the params* array.	Eg, "myFunc@"

enableFirstUp(enable:=1)
; Enable the first key in the long hotkey to trigger its normal press event on its release, in the case the long hotkey wasn't completed.
; Call this function with enable:=0 to disable this behaviour.

ThisLongHotkey()
; Similar to A_ThisHotkey.
; Call this method on the reference passed to the success function, to get back the string that defined the hotkey.
; Eg, A_ThisLongHotkey:=lh.ThisLongHotkey(), where lh is the last parameter of the success function, eg, f(x1,...,xn,lh)

TimeSinceThisLongHotkey()
; Similar to A_TimeSinceThisHotkey.
; Returns the time (in ms) since this lhk was triggered.
; If the long hotkey has never been triggered, this method returns -1.		

unregister()
; Unregister this long hotkey. To free the object, do lh:="" afterwards, if you wish. Here, lh is an instance of the class LongHotkey.
; To reregister, use reregister() method (not if lh:="" was done, obviously).

reregister()
; Reregister a long hotkey, return 1 on success, 0 otherwise.
Example usage,

Code: Select all

myLh:= new LongHotkey(...)
myLh.setContext("WinActive", "ahk_exe chrome.exe") ; Make this long hotkey work while chrome is active.
Class methods.

Code: Select all

suspendAll(bool:=1)
; Pseudo-supend all long hotkeys.
; Call with bool:=1 or do not pass a parameter at all, to invoke suspension.
; Call with bool:=0 to cancel suspension.
; Call with bool:=-1 to toggle suspension.
; Returns the current suspension state, ie., 1 for all is suspended, 0 for not suspended

setGlobalContext(function:="",params*)
; Provide a function with an optional number of parameters to act as global context function.
; If this function is set and returns 0 no hotkey is active.
; This function should return true if the hotkey should be considered as in context, else false.
; Call with setGlobalContext() without any parameters to remove context.

unregisterAll()
; Unregisters all hotkeys.

reregisterAll()
; Reregisters all hotkeys.
	
MostRecentLongHotkey(ref:=0)	
; This is returns the most recently completed long hotkey.
; Call with ref:=0 or without any parameter, to get the key string that defined the hotkey that triggered most recently, eg,
; "a & b & c".
; Call with ref:=1 to recieve a reference to the most recent long hotkey instead.

TimeSinceMostRecentLongHotkey()
; Returns the time since (in ms.) the the most recently lhk was triggered.
; If no long hotkey has been triggered, this method returns -1.

PriorLongHotkey(ref:=0)
; Similar to A_PriorHotkey.
; Call with ref:=0 or without any parameter, to get the key string that defined the hotkey that triggered prior to the most recent one, eg,
; "a & b & c".
; Call with ref:=1 to recieve a reference to the prior long hotkey instead.
; returns blank if no PriorLongHotkey exists.

TimeSincePriorLongHotkey()
; Similar to A_TimeSincePriorHotkey
; Returns the time since (in ms.) the prior lhk was last triggered. That is, time since the lhk prior to the most recent one was triggered.
; If there is no prior long hotkey, this method returns -1.
Example usage,

Code: Select all

if (LongHotkey.PriorLongHotkey() = "a & b & c" && LongHotkey.TimeSincePriorLongHotkey()<1000)
	LongHotkey.suspendAll()
Behaviour and design.
The prefix key loses its native function, just like the built-in custom combination hotkeys, implying that, defining a long hotkey: a & b & c, will suppress the native function of the a-key. However, b and c retain their native functions, unless the complete sequence before them are pressed and held, eg, b doesn't trigger its native function if a is held, and c doesn't if a+b is held. To retain the native function of a key, even if all the keys in the sequence before it is being held, prepend the key names with ~, eg, ~a & b & ~c.

Fire on release, as an alternative to the ~ prefix, you may call the instance method myLh.enableFirstUp() to enable the first key in myLh:s sequence to trigger upon its release, but only if the long hotkey was not completed. (This feature is quite experimental)

Wildcards, you can specify * on some or all keys in a sequence to allow it to trigger regardless whether any modifiers are being held or not.
Eg, *a & *b & *c will trigger on Ctrl+a+b+c, while the corresponding sequence without * will not.

Auto-repeat, holding a completed long hotkey, will make it auto-repeat, at the same rate as its last key is auto-repeating.

Retriggering and releasing parts of the sequence, if you trigger a hotkey, and partially release the sequence, you may retrigger the hotkey by repressing only the part that was released.
Also, partially releasing a non completed sequence, does not reset the whole sequence.

Overlaping sequences, consider,

Code: Select all

"1 & 2 & 3"			; (a)
"1 & 2 & 4"			; (b)
"q & w & e"			; (c)
"q & w & e & r"		; (d)
While holding 1+2, you can alternate between triggering both hotkey (a) and (b), by alternate between pressing the keys 3 and 4. Holding q+w+e will auto-repeat hotkey (c), but when you press and hold r, hotkey (d) will trigger and start to auto-repeat instead. Releasing r, while still holding q+w+e, will not retrigger (c), you'll have to release e and press it again to retrigger (c).

Duplicate hotkeys, are allowed, consider

Code: Select all

new LongHotkey("a & b & c", "myFunc", 37)
new LongHotkey("a & b & c", "myFunc", 42)
pressing a+b+c will trigger both long hotkeys, in the order they are defined, i.e., first myFunc(37) is called, then myFunc(42).

About the implementation.
How it works.
In pseudo-code, demonstrating the simple idea, while hiding most technical details.

Code: Select all

; User makes a new long hotkey:
new LongHotkey("Ctrl & a & b & alt & c","myFunc")
; The class registers these hotkeys:
#If Press()
^a::return
^b::return
!^c::return
; Makes an array:
keyList:=[^a,^b,!^c]
; and a progress counter:
hits:=0
; When user presses ^a,^b or !^c
; Press() is called

Press()
{
	if A_ThisHotkey=keyList[hits+1] ; A_ThisHotkey is either ^a,^b or !^c
	{
		hits++						; If the user pressed the next key in the sequence, the hit count is incremented.
		if hits=keyList.Length()
			myFunc() ; sequence completed, call function.
		else
			Hotkey, If, Release()
			Hotkey, A_ThisHotkey " up", doNothing, On	; Register the up event for this hotkey, such that when the user releases the key, the sequence fails, see Release()
		return 1 ; Native function of A_ThisHotkey is suppressed because it advanced the progress of the sequence.
	}
	hits:=0	 ; user didn't press the next key in the sequence, the sequence failed. 
	return 0 ; Native function of A_ThisHotkey is retained
}

Release()
{
	Hotkey, A_ThisHotkey, Off
	hits=0
	return 0 ; Never suppress the native function of the up event.
}
Interference with regular hotkeys and super-global variables.

When you create a long hotkey, the class registers each key in the sequence as a regular hotkey, with all modifiers that preceeds it, eg, ctrl & a & alt & b will register the hotkeys ^a and !^b. There are no actual & hotkeys used, and ~ is not registred, it is handled differently, but with similar result.
However, you may create both regular and long hotkeys in the same script, the class defines the hotkeys in a context (#if) that will override regular hotkeys native function only if the long hotkey is advancing or completing, but if it is not, it will let the regular hotkeys be executed. Specifying ~ on a key in a long hotkey, will always let the native function of the hotkey execute.
This should be kept in mind when creating both regular and long hotkeys in the same script,
A_ThisHotkey wrote: When a hotkey is first created -- either by the Hotkey command or a double-colon label in the script -- its key name and the ordering of its modifier symbols becomes the permanent name of that hotkey, shared by all variants of the hotkey.
(Tragically,) all non-class and non-input variables in this class has been prepended with an underscore, _, to reduce the risk of interfering with any super-global variables in the users script. Hence, these kinds of declarations should be avoided:

Code: Select all

global _ctr:=37 ; This is my most important number.
Performance.

The key sequences are pre-processed and stored such that at time of evaluation, only array look-ups and short string and numeric comparisons are needed. No regular expression or searching in strings, no GetKeyState or KeyWait or any timings requiered.
The implementation of the "First up" option is a bit shameful, it doesn't seem to matter to much though.

Evaluation time of down-events are more time consuming than up events, however, the down events need less than 1 ms for around 200 long hotkeys, on my ~5 year-old laptop, intel i7.
At around 4000-5000 long hotkeys, I notice that auto-repeating a key that is part of a long hotkey, gets slower.

The time it takes to evaluate the long hotkeys should be proportional to the number of long hotkeys, the length of the sequences shouldn't matter.
Up-events are only registred in case the corresponding down-event advances a sequence, this avoids evaluating up-events every time a key that is part of any long hotkey is released.

Limitations and known issues.
  • The First up option and Capslock key (propbably true for the other "lock" keys aswell) doesn't cooperate well.
  • If you want to use a modifier as the last key in the sequence, don't use the symbol representation, eg, use ... & RAlt instead of ... & >!.
  • The matter of Key rollover. Also see this thread.
  • JoyX buttons does not support the Up suffix, I suppose they could be used as the last key in a sequence though. Not tested.
  • Very limited testing has been done, mostly tested on win7 64bit, AHK 1.1.24.02 64bit Unicode.
Misc.
You are welcome to report any issues you experience and submit your suggestions and questions.
Feel free to use this in any context you wish, good luck!
Last edited by Helgef on 05 Dec 2017, 10:42, edited 3 times in total.
guest3456
Posts: 2496
Joined: 09 Oct 2013, 10:31

Re: [Class] LongHotkeys (a & b & c & ...)

23 Oct 2016, 10:26

well done, great documentation

Helgef
Posts: 3569
Joined: 17 Jul 2016, 01:02
Contact:

Re: [Class] LongHotkeys (a & b & c & ...)

24 Oct 2016, 02:14

guest3456 wrote:well done, great documentation
Thank you, typing the post was half the battle :|
User avatar
Capn Odin
Posts: 1291
Joined: 23 Feb 2016, 19:45
Location: Denmark

Re: [Class] LongHotkeys (a & b & c & ...)

24 Oct 2016, 04:37

It took me most of the soundtrack of Morrowind to read and analyse your post and script, I must confess I was somewhat dubious regarding the claims in the "Performance" section, but after looking at processKeys(str), Press() and RegisterHotkey(key) I think that they in general hold true.
Good work, I will probably link quite a few people to this in the ask for help section.
Please excuse my spelling I am dyslexic.
Helgef
Posts: 3569
Joined: 17 Jul 2016, 01:02
Contact:

Re: [Class] LongHotkeys (a & b & c & ...)

24 Oct 2016, 06:21

Capn Odin wrote:It took me most of the soundtrack of Morrowind to read and analyse your post and script, I must confess I was somewhat dubious regarding the claims in the "Performance" section, but after looking at processKeys(str), Press() and RegisterHotkey(key) I think that they in general hold true.
Good work, I will probably link quite a few people to this in the ask for help section.
Thanks for checking it out!
It is very easy to measure the evaluation time, actually, since press() only returns at one place in the function, unless you specify a global context function. Just take a timestamp from the top. and then check the elapsed time and display it in a tooltip just before the return. I used QPC to measure:

Code: Select all

Loop, 200
	new longhotkey("q & w & e", "test")
Now, pressing q takes ~0.9 ms. Also, note that this is the worst possible case, i.e., all hotkeys start with the same key. doing this instead:

Code: Select all

Loop, 100
{
	new longhotkey("q & w & e", "test")
	new longhotkey("a & w & e", "test")
}
then q drops down to ~0.7 ms. Pressing a key that isn't advancing, eg, w, takes about ~0.5 ms.

You can try doing this, and see if auto-repeating, eg, w gets slowed down.

Code: Select all

Loop, 2000
{
	new longhotkey("q & w & e", "test")
	new longhotkey("a & w & e", "test")
}
Please don't trigger these hotkeys, then you'll get test sent 4000 times. :o
Also, I realise, here I didn't account for the up-event, that will add ~0.6 ms or so, when pressing and releasing a key that begin a sequence.
guest3456
Posts: 2496
Joined: 09 Oct 2013, 10:31

Re: [Class] LongHotkeys (a & b & c & ...)

24 Oct 2016, 09:23

less than 1ms? are you sure about that? i thought 1ms was the smallest possible timeframe that you could even measure. and you're quoting speeds even lower than that...

if speed is an issue, you might want to look into StringReplace instead of RegExReplace. as i rememeber, the RegEx framework has some overhead

Helgef
Posts: 3569
Joined: 17 Jul 2016, 01:02
Contact:

Re: [Class] LongHotkeys (a & b & c & ...)

24 Oct 2016, 14:49

guest3456 wrote:less than 1ms? are you sure about that? i thought 1ms was the smallest possible timeframe that you could even measure. and you're quoting speeds even lower than that...
I'm using QueryPerformanceCounter, as far as I understand, measuring something that takes around one ms, should be quite accurate.
QueryPerformanceCounter wrote: Retrieves the current value of the performance counter, which is a high resolution (<1us) time stamp that can be used for time-interval measurements.
Source: msdn
Anyways, I measured again, this time on 10000 instances, and it takes about 40 ms to evaluate the Press() method, which, when considering the average per instance, yields 0.8 ms per 200 instances. So even if the accuracy of that measurement is just within 1 ms, the correct time would be expected between 0.78 and 0.82 ms.

However, the important thing is not whether it takes 1 or 10 ms, the point is it doesn't take 100 or 1000 ms, then it wouldn't be any good. The comments about the performance is to reasure potential users that they need not to worry about slowing down their computers if they use this class within reasonable limits. I think its pretty unlikley anyone would like 200-1000 instances of this class. Personally, I have none :lol:
guest3456 wrote: if speed is an issue, you might want to look into StringReplace instead of RegExReplace. as i rememeber, the RegEx framework has some overhead
Yes, I think in most cases, if you can do it with StringReplace instead of RegExReplace, there is some performance gain. However, the regexreplacing in this class is only done when you instanciate an object, so it doesn't affect the performance after that.

Also, looking at my answer to Capn Odin, I see that it could easily be interpreted as I'm talking about the time it takes to instanciate the objects, eg, new longhotkey..., I'm not. Sorry for being unclear.

Thanks to both of you for your comments, most appreciated.

Return to “Scripts and Functions”

Who is online

Users browsing this forum: blue83, Lukegotjellyfis and 57 guests