[CLASS] Hotkey command wrapper

Post your working scripts, libraries and tools for AHK v1.1 and older
User avatar
runie
Posts: 304
Joined: 03 May 2014, 14:50
Contact:

[CLASS] Hotkey command wrapper

01 Mar 2017, 05:05

A wrapper for the Hotkey command.

This focuses on ease of use and OOP when working with hotkeys.

Github repo


Usage:

Code: Select all

MyHtk := new Hotkey(Key, Target [, Window, Type])
MyHtk.Enable() - enable hotkey
MyHtk.Disable() - disable hotkey
MyHtk.Delete() - delete hotkey, call this when done with a hotkey and you want to release related objects.

Base methods:
Instance := Hotkey.GetKey(Key [, Window, Type]) - Get a hotkey instance from properties
Hotkey.EnableAll()
Hotkey.DisableAll()
Hotkey.DeleteAll()
Example:

Code: Select all

#SingleInstance force
#NoEnv
#Persistent

#Include Class Hotkey.ahk

; the simplest hotkey we can have is a key that runs a function or label
; lets make a hotkey that binds ALT+A to an empty msgbox.
new Hotkey("!A", Func("MsgBox"))

; now lets make a hotkey that opens up a msgbox when I press ALT+A and notepad is open.
; but oh no! it's the same key as above!
; well, luckily we can have different types of hotkeys. this one will only run if notepad is active. otherwise, that first one will run.
NotepadHtk := new Hotkey("!A", Func("MsgBox").Bind("Notepad!"), "ahk_class Notepad")
; with the instance, we can modify the hotkey.
; to disable it, we could do NotepadHtk.Disable()

; lets create another hotkey which toggles the previous hotkey.
; we do this by binding this hotkey to the previous hotkeys' Toggle method.
; this hotkey is also only active when notepad is active.
new Hotkey("!S", NotepadHtk.Toggle.Bind(NotepadHtk), "ahk_class Notepad")

; lets also create a hotkey that only runs when notepad is *NOT* active:
; this hotkey will make a beep at 500hz
new Hotkey("!D", Func("SoundBeep").Bind(500), "ahk_class Notepad", "NotActive")

; lets make one last hotkey.
; this one will delete every hotkey except for the notepad one.
; this one is bound to a label instead of a function/method.
new Hotkey("!F", "DisableSomeKeys")
return


DisableSomeKeys:
; we use the base object method Hotkey.GetKey() to retrieve the hotkey instance.
Hotkey.GetKey("!A").Delete() ; deletes the first hotkey.
Hotkey.GetKey("!S", "ahk_class Notepad").Delete() ; deletes the third hotkey.

; of course, you can save the instance returned by GetKey() to a variable if you want.
HtkInstance := Hotkey.GetKey("!F")
HtkInstance.Delete()

; lets also make sure the notepad hotkey is enabled in case it was disabled when this label runs.
; we still have the instance from when we made this hotkey.
if !NotepadHtk.Enabled
	NotepadHtk.Enable()

MsgBox("Keys disabled!")
return


MsgBox(Msg := "") {
	MsgBox,, Hotkey example, % Msg
}

SoundBeep(hz) {
	SoundBeep % hz, 200
}
Last edited by runie on 02 May 2017, 13:24, edited 8 times in total.
A_AhkUser
Posts: 1147
Joined: 06 Mar 2017, 16:18
Location: France
Contact:

Re: [CLASS] Hotkey command wrapper

26 Apr 2017, 18:53

Hi Run1e,

Thanks for sharing it. I encoutered a problem with hotkeys when it comes to releasing the object references and resolved to make a wrapper for the Hotkey command, keeping track of them, in order to resolve it. Instead of starting from scratch I decided to take a glance on the forum and hopefully just found yours. I like it especiallly as it isn't pretentious and despite that efficient.

At least one suggestion, concerning your label HotkeyHandler. I suggest, in order to be allowed to include this script either structure your Class-script like this:

Code: Select all

Gosub,  MyScriptEnd
return

Hotkey {
; ...
}
HotkeyHandler:
; ...
return

MyScriptEnd
or encapsulate your HotkeyHandler label:

Code: Select all

class Hotkey {
	static Keys := [] ; keep track of hotkeys
	
	; bind a hotkey to a function/label
	Bind(Key, Target, HWND := "") { ; target = label or function reference
		if this.Keys[Key].Disabled ; disable if hotkey already exists
			this.Enable(Key)
		Hotkey, IfWinActive, % ((HWND+=0) ? "ahk_id" HWND : "") ; HWND+=0 forces hex to dec
		Label := (IsLabel(Target) ? Target : this.HotkeyHandler.bind(this, Key))
		Hotkey, % Key, % Label, UseErrorLevel
		return !ErrorLevel ? this.Keys[Key] := {Target:Target, HWND:HWND} : ErrorLevel
	}
	
	; enable a hotkey
	Enable(Key) {
		Hotkey, IfWinActive, % (this.Keys[Key].HWND ? "ahk_id" this.Keys[Key].HWND : "")
		Hotkey, % Key, On, UseErrorLevel
		return ErrorLevel ? ErrorLevel : this.Keys[Key].Remove("Disabled")
	}
	
	; disable a hotkey
	Disable(Key) {
		Hotkey, IfWinActive, % (this.Keys[Key].HWND ? "ahk_id" this.Keys[Key].HWND : "")
		Hotkey, % Key, Off, UseErrorLevel
		return ErrorLevel ? ErrorLevel : !(this.Keys[Key].Disabled := true)
	}
	
	; rebind an existing hotkey
	Rebind(Key, NewKey) {
		if a := this.Disable(Key)
			return a
		return this.Bind(NewKey, this.Keys[Key].target, this.Keys[Key].HWND)
	}
	
	HotkeyHandler(Key) {
	this.Keys[Key].Target.Call()
	}
}
even though encapsulating it arise the same problem when it comes to releasing the object references.
Actually, this way it can be include without launching the label as side-effect.

Amazing! Many thanks.
my scripts
User avatar
runie
Posts: 304
Joined: 03 May 2014, 14:50
Contact:

Re: [CLASS] Hotkey command wrapper

26 Apr 2017, 20:37

A_AhkUser wrote:Hi Run1e,

Thanks for sharing it. I encoutered...
Hey.

I didn't think of encapsulating it. It's quite a while since I wrote this so that's probably why I still used a label.
I updated the code.

If you're interested in OOP solutions for inbuilt AHK features, I suggest you take a look at my GUI OOP wrapper aswell :)
A_AhkUser wrote:even though encapsulating it arise the same problem when it comes to releasing the object references.
Could you elaborate on this? Is there a point in freeing object references? I actually don't know :shock:

I guess you could add a method like this:

Code: Select all

Delete(Key) {
	this.Disable(Key)
	this.Keys[Key] := ""
}
However this would not allow you to enable a disabled hotkey.
A_AhkUser
Posts: 1147
Joined: 06 Mar 2017, 16:18
Location: France
Contact:

Re: [CLASS] Hotkey command wrapper

27 Apr 2017, 13:48

Run1e wrote:Is there a point in freeing object references?
Is that when an object is destroyed (when the last reference to an object is being released), __Delete is called: __Delete will not be called unless all references to the object are freed. I mean for example, the same way, we have:

Code: Select all

SetTimer, MyLabel, Delete
Concerning your GUI OOP wrapper it is really exhaustive, thanks for sharing it:) However it is a poisoned chalice: now i just want to rewrite all my (now) obsolescent script using GUI command... lol
my scripts
User avatar
Soft
Posts: 174
Joined: 07 Jan 2015, 13:18
Location: Seoul
Contact:

Re: [CLASS] Hotkey command wrapper

27 Apr 2017, 23:32

code has to break all circular references before calling __Delete() or whatever exit function, otherwise __Delete() won't be called by putting an empty value(eg Instance := "")

For the same reason, gui wrapper doesn't seem to call __Delete() properly.Besides, I think it is better to add a method that unregisters boundfunc (guicontrol, -g ...) for future use
AutoHotkey & AutoHotkey_H v1.1.22.07
User avatar
runie
Posts: 304
Joined: 03 May 2014, 14:50
Contact:

Re: [CLASS] Hotkey command wrapper

28 Apr 2017, 05:32

Soft wrote:code has to break all circular references before calling __Delete() or whatever exit function, otherwise __Delete() won't be called by putting an empty value(eg Instance := "")

For the same reason, gui wrapper doesn't seem to call __Delete() properly.Besides, I think it is better to add a method that unregisters boundfunc (guicontrol, -g ...) for future use
Thanks for the explanation and tips, I fixed the GUI wrapper. The boundfuncs and reference in Gui.Instances is now removed in the Destroy method.

https://github.com/Run1e/PowerPlay/blob ... %20GUI.ahk
A_AhkUser
Posts: 1147
Joined: 06 Mar 2017, 16:18
Location: France
Contact:

Re: [CLASS] Hotkey command wrapper

28 Apr 2017, 20:35

I update the post I linked in my first reply (hotkey command (release the last references to an object)). I just knocked up a wrapper for the Hotkey command, where both Hotkeys and Hotkey Classes are in debt to your original work. I implemented a delete method that actually release last references that might be caught due to having assigned a boundfunc as hotkey label -- and which allows __Delete meta-function to be called. For sure, it is in accordance with the purpose I pursue but I hope it could be productive to your Hotkey wrapper to take a glance at it. Thanks again ;)
my scripts
User avatar
runie
Posts: 304
Joined: 03 May 2014, 14:50
Contact:

Re: [CLASS] Hotkey command wrapper

29 Apr 2017, 03:03

A_AhkUser wrote:I update the post I linked in my first...
Well, you inspired me to say the least. I redid my whole Hotkey class, here it is:
https://github.com/Run1e/PowerPlay/blob ... Hotkey.ahk

Notes:
Storing the instance when creating a new hotkey is not necessary. It can be fetched later via Hotkey.Keys[Type, Win, Key] or if it's global, via Hotkey.GetGlobal(Key)
It's all in one class, so you can use the base object to call stuff like Hotkey.DeleteAll() to delete every hotkey.

Regarding your class:
It's gonna break if you have the same key for the same window, but different context type. For example binding a key to a window using IfWinActive and IfWinNotActive is gonna break for you. Adding another level to your array containing the key information will fix this.
I also think your class is gonna break if you bind a key, delete it, and attempt to bind it again (Hotkeys have to be enabled before they can be bound again). I didn't test this but it should happen.

I'd love if you'd try my class out. I think it does everything you needed that lacked from my original class (it also made some things in Power Play better :D).

I implemented the class into Power Play if you want to see a practical implementation of it: https://github.com/Run1e/PowerPlay

I'll create an example and update the OP when I'm sure there's no obvious bugs in it.
A_AhkUser
Posts: 1147
Joined: 06 Mar 2017, 16:18
Location: France
Contact:

Re: [CLASS] Hotkey command wrapper

29 Apr 2017, 14:39

I had feeling that make two classes wasn't an ideal solution since, after all, Hotkeys class is a simple object like any others. Certainly, it is instead more pertinent and intuitive make a all in one class, making some of the methods call via base object. Besides, you're absolutly right for the three-based criteria Type, Win, Key since you can actually have the same key for the same window, but different context type.

Some notes regarding your brand-new Hotkey class:

. I think you mixed ErrorLevel up (2 and 1) order in __New meta-function. It complies with writing order but not parameters' one - at least if your aim was to set ErrorLevel depending on the parameter which will otherwise give birth to an error.

. Didn't know the shorthand (~=) for RegExMatch- very usefull. By the way, I suggest im)^(Not)?(Active|Exist)$ as needleregex since yours matchs also, for instance, _winactive - unless it is by design.

. Concerning the delete method: have you tested it with the example I provided? Your delete method does not release the caught references (the msgbox from __Delete meta-function isn't displayed). As Soft already said __Delete() will not be called by putting an empty value (eg Instance := ""). Here's my attempt based on your new class:

Code: Select all

delete() {
static __f := Func("WinActive") ; just some random function to be set as label to release the last references that might be caught.

	if this.disable() {
		if this.Apply(__f) {
			Hotkey.Keys[this.Type, this.Win].Remove(this.Key)
		return true
		} return false
	} return false
}

Anyway thank you very much, I'll use yours in my project for now on ;)
my scripts
User avatar
runie
Posts: 304
Joined: 03 May 2014, 14:50
Contact:

Re: [CLASS] Hotkey command wrapper

29 Apr 2017, 15:15

A_AhkUser wrote:I had feeling that make two classes wasn't an ideal solution since, after all...
The ErrorLevel values seems to be properly placed to me. Could you elaborate?

I added the regex and (probably?) fixed the caught references problem. Note: if you save the instance when creating the hotkey, you'll have to clear that instance variable as well before the caught reference can be freed.

Edit:
I figured out an obvious way of making instances work without stuck references. Put the boundfunc into the hotkey funcobject itself instead of saving it in the instance (in other words: binding the key to this.Handler(this, boundfunc) instead of saving the target in the instance and doing this.Target.Call() in the handler method. I also renamed the Handler method to CallFunc.
Now you can save instances instantly and not worry about caught references. *updated code*

Here's an example with the current class:

Code: Select all

#SingleInstance force
#NoEnv

global asd, hkey

asd := new test

hkey := new Hotkey("a", asd.msg.bind(asd))
return

next:
asd := "" ; will fail
;Hotkey.GetGlobal("a").Delete()
hkey.Delete()
asd := "" ; will succeed
return

class test {
	__New() {
		m("new")
	}
	
	__Delete() {
		m("delete")
	}
	
	msg() {
		m("msg")
		gosub next
	}
}

; all methods return false on failure
Class Hotkey {
	static Keys := {} ; keep track of instances
	static KeyEnabled := {} ; keep track of state of hotkeys
	
	__New(Key, Target, Win := false, Type := "Active") {
		
		; check input
		if !StrLen(Win)
			Win := false
		if !StrLen(Key)
			return false, ErrorLevel := 2
		if !(Bind := IsLabel(Target) ? Target : this.CallFunc.Bind(this, Target))
			return false, ErrorLevel := 1
		if !(Type ~= "im)^(Not)?(Active|Exist)$")
			return false, ErrorLevel := 4
		
		; set values
		this.Key := Key
		this.Win := Win
		this.Type := Type
		
		; enable if previously disabled
		if (Hotkey.KeyEnabled[Type, Win, Key] = false)
			this.Apply("On")
		
		; bind the key
		if !this.Apply(Bind)
			return false
		
		this.Enabled := true ; set to enabled
		return Hotkey.Keys[Type, Win, Key] := this
	}
	
	; 'delete' a hotkey. call this when you're done with a hotkey
	; this is superior to Disable() as it releases the function references
	Delete() {
		static JunkFunc := Func("WinActive")
		if this.Disable()
			if this.Apply(JunkFunc)
				return true, Hotkey.Keys[this.Type, this.Win].Remove(this.Key)
		return false
	}
	
	; enable hotkey
	Enable() {
		if this.Apply("On")
			return this.Enabled := true
		return false
	}
	
	; disable hotkey
	Disable() {
		if this.Apply("Off")
			return true, this.Enabled := false
		return false
	}
	
	; toggle enabled/disabled
	Toggle() {
		return this.Apply(this.Enabled ? "Off" : "On")
	}
	
	; ===== CALLED VIA BASE OBJECT =====
	
	; enable all hotkeys
	EnableAll() {
		Hotkey.CallAll("Enable")
	}
	
	; disable all hotkeys
	DisableAll() {
		Hotkey.CallAll("Disable")
	}
	
	; delete all hotkeys
	DeleteAll() {
		Hotkey.CallAll("Delete")
	}
	
	; get a global hotkey from it's key name
	GetGlobal(Key) {
		return Hotkey.Keys["Active", false, Key]
	}
	
	; ===== PRIVATE =====
	
	Enabled[] {
		get {
			return Hotkey.KeyEnabled[this.Type, this.Win, this.Key]
		}
		
		set {
			return Hotkey.KeyEnabled[this.Type, this.Win, this.Key] := value
		}
	}
	
	CallFunc(Target) {
		Target.Call()
	}
	
	Apply(Label) {
		Hotkey, % "IfWin" this.Type, % this.Win ? this.Win : ""
		if ErrorLevel
			return false
		Hotkey, % this.Key, % Label, UseErrorLevel
		if ErrorLevel
			return false
		return true
	}
	
	CallAll(Method) {
		Instances := []
		for Index, Type in Hotkey.Keys
			for Index, Win in Type
				for Index, Htk in Win
					Instances.Push(Htk)
		for Index, Instance in Instances
			Instance[Method].Call(Instance)
	}
}

pa(array, depth=5, indentLevel:="   ") { ; tidbit, this has saved my life
	try {
		for k,v in Array {
			lst.= indentLevel "[" k "]"
			if (IsObject(v) && depth>1)
				lst.="`n" pa(v, depth-1, indentLevel . "    ")
			else
				lst.=" => " v
			lst.="`n"
		} return rtrim(lst, "`r`n `t")	
	} return
}

m(x*) {
	for a, b in x
		text .= (IsObject(b)?pa(b):b) "`n"
	MsgBox, 0, msgbox, % text
}
A_AhkUser
Posts: 1147
Joined: 06 Mar 2017, 16:18
Location: France
Contact:

Re: [CLASS] Hotkey command wrapper

30 Apr 2017, 15:43

Run1e wrote:The ErrorLevel values seems to be properly placed to me. Could you elaborate?

Code: Select all

__New(Key, Target, Win := false, Type := "Active") {
		
	; check input
	if !StrLen(Win)
		Win := false
	if !StrLen(Key)
		return false, ErrorLevel := 2 ; 1 Key is the first param
	if !(Bind := IsLabel(Target) ? Target : this.CallFunc.Bind(this, Target))
		return false, ErrorLevel := 1 ; 2 Key, Target is the second param
	if !(Type ~= "im)^(Not)?(Active|Exist)$")
		return false, ErrorLevel := 4 ; Type is the fourth param: ok
	; ...
	
}
I figured out an obvious way of making instances work without stuck references. (...)
You right it works! Good job;)
my scripts
User avatar
runie
Posts: 304
Joined: 03 May 2014, 14:50
Contact:

Re: [CLASS] Hotkey command wrapper

01 May 2017, 03:56

Oh. I used the ErrorLevel values given at the Hotkey command page.
A_AhkUser wrote:You right it works! Good job;)
:dance:

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: No registered users and 160 guests