├── .GitIgnore ├── AppFactory_H ├── Examples │ ├── test.ahk │ ├── Simplest Example.ahk │ └── Example.ahk └── Source │ ├── BindModeThread.ahk │ ├── JSON.ahk │ ├── InputThread.ahk │ └── AppFactory.ahk ├── InputControl-Binding.gif ├── InputControl-EditBox.png ├── .gitattributes ├── AppFactory ├── Simple Example.ahk ├── Function Binding Example.ahk ├── BindModeThread.ahk ├── JSON.ahk ├── InputThread.ahk └── AppFactory.ahk └── README.md /.GitIgnore: -------------------------------------------------------------------------------- 1 | *.ini 2 | *.bak 3 | -------------------------------------------------------------------------------- /AppFactory_H/Examples/test.ahk: -------------------------------------------------------------------------------- 1 | msgbox -------------------------------------------------------------------------------- /InputControl-Binding.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evilC/AppFactory/HEAD/InputControl-Binding.gif -------------------------------------------------------------------------------- /InputControl-EditBox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evilC/AppFactory/HEAD/InputControl-EditBox.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /AppFactory/Simple Example.ahk: -------------------------------------------------------------------------------- 1 | #SingleInstance force 2 | #NoEnv 3 | #Include AppFactory.ahk 4 | 5 | factory := new AppFactory() 6 | factory.AddInputButton("MyHotkey", "w200", Func("SendGreeting")) 7 | factory.AddControl("UserName", "Edit", "xm w200") 8 | Gui, Show, x0 y0 9 | return 10 | 11 | SendGreeting(state){ 12 | global factory 13 | if (state){ ; When the key is pressed 14 | name := factory.GuiControls.UserName.Get() ; Get the value of the Edit box 15 | Send Hi, My name is %name% ; Send greeting 16 | } 17 | } 18 | 19 | ^Esc:: 20 | GuiClose: 21 | ExitApp 22 | -------------------------------------------------------------------------------- /AppFactory_H/Examples/Simplest Example.ahk: -------------------------------------------------------------------------------- 1 | #SingleInstance force 2 | #NoEnv 3 | #Include ..\Source\AppFactory.ahk 4 | 5 | factory := new AppFactory() 6 | factory.AddInputButton("HK1", "w200", Func("InputEvent")) 7 | factory.AddControl("MyEdit", "Edit", "xm w200", "Default Value", Func("GuiEvent")) 8 | 9 | Gui, Show, x0 y0 10 | return 11 | 12 | InputEvent(state){ 13 | Global factory 14 | Tooltip % "Input changed state to: " state " after " A_TickCount " ticks" 15 | } 16 | 17 | GuiEvent(state){ 18 | Tooltip % "GuiControl changed state to: '" state "' after " A_TickCount " ticks" 19 | } 20 | 21 | ^Esc:: 22 | GuiClose: 23 | ExitApp -------------------------------------------------------------------------------- /AppFactory/Function Binding Example.ahk: -------------------------------------------------------------------------------- 1 | #SingleInstance force 2 | #NoEnv 3 | #Include AppFactory.ahk 4 | 5 | factory := new AppFactory() 6 | factory.AddInputButton("HK1", "w200", Func("InputEvent").Bind("1")) 7 | factory.AddInputButton("HK2", "xm w200", Func("InputEvent").Bind("2")) 8 | factory.AddControl("MyEdit", "Edit", "xm w200", "Default Value", Func("GuiEvent").Bind("MyEdit")) 9 | factory.AddControl("MyDDL", "DDL", "xm w200 AltSubmit", "One||Two|Three", Func("GuiEvent").Bind("MyDDL")) 10 | 11 | Gui, Show, x0 y0 12 | return 13 | 14 | InputEvent(ctrl, state){ 15 | Global factory 16 | Tooltip % "Input " ctrl " changed state to: " state " after " A_TickCount " ticks while MyEdit was '" factory.GuiControls.MyEdit.Get() "'" 17 | } 18 | 19 | GuiEvent(ctrl, state){ 20 | Tooltip % "GuiControl " ctrl " changed state to: '" state "' after " A_TickCount " ticks" 21 | } 22 | 23 | ^Esc:: 24 | GuiClose: 25 | ExitApp 26 | -------------------------------------------------------------------------------- /AppFactory_H/Examples/Example.ahk: -------------------------------------------------------------------------------- 1 | #SingleInstance force 2 | #NoEnv 3 | #Include ..\Source\AppFactory.ahk 4 | 5 | ; Un-comment these lines if you want to compile. You may need to tweak the first parameter 6 | ;FileInstall, ..\Source\BindModeThread.ahk, BindModeThread.ahk 7 | ;FileInstall, ..\Source\InputThread.ahk, InputThread.ahk 8 | 9 | factory := new AppFactory() 10 | factory.AddInputButton("HK1", "w200", Func("InputEvent").Bind("1")) 11 | factory.AddInputButton("HK2", "xm w200", Func("InputEvent").Bind("2")) 12 | factory.AddControl("MyEdit", "Edit", "xm w200", "Default Value", Func("GuiEvent").Bind("MyEdit")) 13 | factory.AddControl("MyDDL", "DDL", "xm w200 AltSubmit", "One||Two|Three", Func("GuiEvent").Bind("MyDDL")) 14 | 15 | Gui, Show, x0 y0 16 | return 17 | 18 | InputEvent(ctrl, state){ 19 | Global factory 20 | Tooltip % "Input " ctrl " changed state to: " state " after " A_TickCount " ticks while MyEdit was '" factory.GuiControls.MyEdit.Get() "'" 21 | } 22 | 23 | GuiEvent(ctrl, state){ 24 | Tooltip % "GuiControl " ctrl " changed state to: '" state "' after " A_TickCount " ticks" 25 | } 26 | 27 | ^Esc:: 28 | GuiClose: 29 | ExitApp 30 | -------------------------------------------------------------------------------- /AppFactory/BindModeThread.ahk: -------------------------------------------------------------------------------- 1 | class _BindMapper { 2 | DetectionState := 0 3 | static IOClasses := {AHK_Common: 0, AHK_KBM_Input: 0, AHK_JoyBtn_Input: 0, AHK_JoyHat_Input: 0} 4 | __New(Callback){ 5 | this.Callback := Callback 6 | ; Instantiate each of the IOClasses specified in the IOClasses array 7 | for name, state in this.IOClasses { 8 | ; Instantiate an instance of a class that is a child class of this one. Thanks to HotkeyIt for this code! 9 | ; Replace each 0 in the array with an instance of the relevant class 10 | call:=this.base[name] 11 | this.IOClasses[name] := new call(this.Callback) 12 | ; debugging string 13 | if (i) 14 | names .= ", " 15 | names .= name 16 | i++ 17 | } 18 | if (i){ 19 | OutputDebug % "UCR| Bind Mode Thread loaded IOClasses: " names 20 | } else { 21 | OutputDebug % "UCR| Bind Mode Thread WARNING! Loaded No IOClasses!" 22 | } 23 | } 24 | 25 | ; A request was received from the main thread to set the Dection state 26 | SetDetectionState(state, IOClassMappings){ 27 | if (state == this.DetectionState) 28 | return 29 | for name, ret in IOClassMappings { 30 | ;OutputDebug % "UCR| BindModeThread Starting watcher " name " with return type " ret 31 | this.IOClasses[name].SetDetectionState(state, ret) 32 | } 33 | this.DetectionState := state 34 | } 35 | 36 | ; Converts an Indexed array of objects to an associative array 37 | ; If you pass an associative array via ObjShare, you cannot enumerate it 38 | ; So it is converted to an indexed array of objects, this converts it back. 39 | IndexedToAssoc(arr){ 40 | ret := {} 41 | Loop % arr.length(){ 42 | obj := arr[A_Index], ret[obj.k] := obj.v 43 | } 44 | return ret 45 | } 46 | 47 | ; ================================================================================================================== 48 | 49 | class AHK_Common { 50 | __New(callback){ 51 | this.Callback := callback 52 | } 53 | 54 | SetDetectionState(state, ReturnIOClass){ 55 | ;OutputDebug % "Turning Hotkeys " (state ? "On" : "Off") 56 | ;~ Suspend, % (state ? "Off", "On") 57 | } 58 | } 59 | 60 | ; ================================================================================================================== 61 | class AHK_KBM_Input { 62 | static IOClass := "AHK_KBM_Input" 63 | DebugMode := 2 64 | 65 | __New(callback){ 66 | this.Callback := callback 67 | this.CreateHotkeys() 68 | } 69 | 70 | SetDetectionState(state, ReturnIOClass){ 71 | ;this.ReturnIOClass := ( state ? ReturnIOClass : 0) 72 | this.ReturnIOClass := ReturnIOClass 73 | 74 | } 75 | 76 | ; Binds a key to every key on the keyboard and mouse 77 | ; Passes VK codes to GetKeyName() to obtain names for all keys 78 | ; List of VKs: https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx 79 | ; Keys are stored in the settings file by VK number, not by name. 80 | ; AHK returns non-standard names for some VKs, these are patched to Standard values 81 | ; Numpad Enter appears to have no VK, it is synonymous with Enter (VK0xD). Seeing as VKs 0xE to 0xF are Undefined by MSDN, we use 0xE for Numpad Enter. 82 | CreateHotkeys(){ 83 | hotkey, If, _AppFactoryBindMode 84 | static replacements := {33: "PgUp", 34: "PgDn", 35: "End", 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 45: "Insert", 46: "Delete"} 85 | static pfx := "$*" 86 | static updown := [{e: 1, s: ""}, {e: 0, s: " up"}] 87 | ; Cycle through all keys / mouse buttons 88 | Loop 256 { 89 | ; Get the key name 90 | i := A_Index 91 | code := Format("{:x}", i) 92 | if (ObjHasKey(replacements, i)){ 93 | n := replacements[i] 94 | } else { 95 | n := GetKeyName("vk" code) 96 | } 97 | if (n = "") 98 | continue 99 | ; Down event, then Up event 100 | Loop 2 { 101 | blk := this.DebugMode = 2 || (this.DebugMode = 1 && i <= 2) ? "~" : "" 102 | fn := this.InputEvent.Bind(this, updown[A_Index].e, i) 103 | hotkey, % pfx blk n updown[A_Index].s, % fn, % "On" 104 | } 105 | } 106 | i := 14, n := "NumpadEnter" ; Use 0xE for Nupad Enter 107 | Loop 2 { 108 | blk := this.DebugMode = 2 || (this.DebugMode = 1 && i <= 2) ? "~" : "" 109 | fn := this.InputEvent.Bind(this, updown[A_Index].e, i) 110 | hotkey, % pfx blk n updown[A_Index].s, % fn, % "On" 111 | } 112 | /* 113 | ; Cycle through all Joystick Buttons 114 | Loop 8 { 115 | j := A_Index 116 | Loop % this.JoystickCaps[j].btns { 117 | btn := A_Index 118 | n := j "Joy" A_Index 119 | fn := this._JoystickButtonDown.Bind(this, 1, 2, btn, j) 120 | hotkey, % pfx n, % fn, % "On" 121 | } 122 | } 123 | */ 124 | critical off 125 | } 126 | 127 | InputEvent(e, i){ 128 | ;tooltip % "code: " i ", e: " e 129 | this.Callback.Call(e, i, 0, this.ReturnIOClass) 130 | } 131 | } 132 | 133 | ; ================================================================================================================== 134 | class AHK_JoyBtn_Input { 135 | static IOClass := "AHK_JoyBtn_Input" 136 | DebugMode := 1 137 | JoystickCaps := [] 138 | 139 | __New(callback){ 140 | this.Callback := callback 141 | this.CreateHotkeys() 142 | } 143 | 144 | SetDetectionState(state, ReturnIOClass){ 145 | ;this.ReturnIOClass := ( state ? ReturnIOClass : 0) 146 | this.ReturnIOClass := ReturnIOClass 147 | } 148 | 149 | ; Binds a key to every key on the keyboard and mouse 150 | ; Passes VK codes to GetKeyName() to obtain names for all keys 151 | ; List of VKs: https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx 152 | ; Keys are stored in the settings file by VK number, not by name. 153 | ; AHK returns non-standard names for some VKs, these are patched to Standard values 154 | ; Numpad Enter appears to have no VK, it is synonymous with Enter (VK0xD). Seeing as VKs 0xE to 0xF are Undefined by MSDN, we use 0xE for Numpad Enter. 155 | CreateHotkeys(){ 156 | static updown := [{e: 1, s: ""}, {e: 0, s: " up"}] 157 | this.GetJoystickCaps() 158 | Loop 8 { 159 | j := A_Index 160 | Loop % this.JoystickCaps[j].btns { 161 | btn := A_Index 162 | n := j "Joy" A_Index 163 | fn := this.InputEvent.Bind(this, 1, btn, j) 164 | hotkey, % n, % fn, % "On" 165 | fn := this.InputEvent.Bind(this, 0, btn, j) 166 | hotkey, % n " up", % fn, % "On" 167 | } 168 | } 169 | } 170 | 171 | GetJoystickCaps(){ 172 | Loop 8 { 173 | cap := {} 174 | cap.btns := GetKeyState(A_Index "JoyButtons") 175 | this.JoystickCaps.push(cap) 176 | } 177 | } 178 | 179 | InputEvent(e, i, deviceid){ 180 | this.Callback.Call(e, i, deviceid, this.ReturnIOClass) 181 | } 182 | } 183 | 184 | ; ================================================================================================================== 185 | class AHK_JoyHat_Input { 186 | static IOClass := "AHK_JoyHat_Input" 187 | DebugMode := 1 188 | HatStrings := {} 189 | 190 | __New(callback){ 191 | this.Callback := callback 192 | Loop 8 { 193 | ji := GetKeyState(A_Index "JoyInfo") 194 | if (InStr(ji, "P")){ 195 | this.HatStrings[A_Index "JoyPov"] := {DeviceID: A_Index, State: -1} 196 | } 197 | } 198 | this.HatWatcherFn := this.HatWatcher.Bind(this) 199 | } 200 | 201 | 202 | SetDetectionState(state, ReturnIOClass){ 203 | ;this.ReturnIOClass := ( state ? ReturnIOClass : 0) 204 | this.ReturnIOClass := ReturnIOClass 205 | fn := this.HatWatcherFn 206 | t := state ? 10 : "Off" 207 | SetTimer, % fn, % t 208 | } 209 | 210 | HatWatcher(){ 211 | for bindstring, obj in this.HatStrings { 212 | state := GetKeyState(bindstring) 213 | if (obj.state == -1 && state != -1){ 214 | ; Press 215 | e := 1 216 | } else if (obj.state != -1 && state == -1){ 217 | ; Release 218 | e := 0 219 | } else { 220 | ; No Change / Bad values 221 | continue 222 | } 223 | i := state == -1 ? -1 : (round(state / 9000) + 1) 224 | DeviceID := obj.DeviceID 225 | obj.state := state 226 | this.Callback.Call(e, i, deviceid, this.ReturnIOClass) 227 | } 228 | } 229 | } 230 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppFactory 2 | A library for creating Apps with configurable inputs and settings. 3 | 4 | ## [Discussion Thread on AHK forums](https://www.autohotkey.com/boards/viewtopic.php?t=38651) 5 | 6 | ## What does it do? 7 | In a typical AutoHotkey script, you declare a hotkey, and have it call some code when that hotkey is pressed, eg: 8 | ``` 9 | F1:: 10 | Send Hi, my name is evilC 11 | return 12 | ``` 13 | However, if you wish to distribute your scripts to other people, each person may want to select their own hotkey (ie they may not want to use the `F1` key to send `Hi, my name is evilC`, they may want to use `Page Up` 14 | Normally, this would require the end-user to edit the AutoHotkey script, and they may not know how to do this. 15 | AppFactory solves this by allowing the end-user to select which Hotkey they wish to use by using a custom GuiControl. 16 | This selection is then saved to a settings file, so that next time the script is run, it remembers the user's selection. 17 | An AppFactory equivalent of the above code would be something like: 18 | ``` 19 | factory.AddInputButton("MyHotkey", "w200", Func("TypeGreeting")) ; Add user-selectable hotkey GuiControl, point it at "TypeGreeting" function 20 | ; ... 21 | TypeGreeting(state){ 22 | if (state){ ; When key is pressed 23 | Send Hi, my name is evilC 24 | } 25 | } 26 | ``` 27 | 28 | This would add a GuiControl like this to the script's GUI: 29 | ![](https://github.com/evilC/AppFactory/blob/master/InputControl-Binding.gif) 30 | When the user clicks the GuiControl, they are presented with a menu with the following options: 31 | **Select Binding** 32 | Allows to press the key combination they wish to use. 33 | **Block** 34 | Suppresses the underlying function of the key 35 | eg for the hotkey `a`, then Block On is equivalent to a hotkey of `a::` and Block Off is equivalent to a hotkey of `~a::` 36 | **Wild** 37 | Allows the hotkey to fire, even if modifiers are held. 38 | eg for the hotkey `a`, then Wild On is equivalent to a hotkey of `*a::` and Wild Off is equivalent to a hotkey of `a::` 39 | **Suppress Repeats** 40 | Off: If the user holds the key, the hotkey will be repeatedly fired, due to key repeat. 41 | On: If the user holds the key, the hotkey will only be fired once. 42 | **Clear** 43 | Removes the hotkey binding 44 | 45 | AppFactory also allows the script author to add "Persistent GuiControls" to their scripts (Edit boxes, check boxes, drop down lists etc) and the state of these are also saved to the settings file. This allows you to easily add configurable options to your scripts. 46 | Extending the above example, everybody is obviously not called `evilC`, so you would probably want to allow users of your script to set their own name. This can be done using a custom EditBox: 47 | 48 | ``` 49 | factory.AddInputButton("MyHotkey", "w200", Func("TypeGreeting")) ; Add user-selectable hotkey GuiControl, point it at "TypeGreeting" function 50 | factory.AddControl("UserName", "Edit", "xm w200") ; Add user-editable EditBox GuiControl 51 | ; ... 52 | TypeGreeting(state){ 53 | Global factory ; Allow this function to see the appfactory object 54 | if (state){ ; When key is pressed 55 | name := factory.GuiControls.UserName.Get() ; Get value from the UserName GuiControl 56 | Send Hi, my name is %name% 57 | } 58 | } 59 | ``` 60 | Which would make the script look like this: 61 | ![](https://github.com/evilC/AppFactory/blob/master/InputControl-EditBox.png) 62 | 63 | ## AutoHotkey versions 64 | ### AHK_L (Regular AutoHotkey) 65 | Use AHK_L 1.x from [here](http://AutoHotkey.com) 66 | Use the version from the `AppFactory` folder. 67 | 68 | ### AHK_H 69 | Use the version from the `AppFactory_H` folder. 70 | You will need [AutoHotkey_H](http://hotkeyit.github.io/v2/) (Supports v1 only) 71 | To package a script for release: 72 | From the AHK_H distro `ahkdll-v1-release-master.zip` in the folder `ahkdll-v1-release-master\Win32a`, copy `AutoHotkey.exe` and `msvcr100.dll` to your script folder. 73 | 74 | Then rename `AutoHotkey.exe` to match your script, eg for `MyScript.ahk`, call it `MyScript.exe` 75 | If a new version of AHK comes out, you may or may not need to replace the EXE again, but the DLL should not change. 76 | 77 | ### Usage 78 | #### Including the library 79 | All scripts must reference the AppFactory library to be able to use it's functions 80 | ##### AHK_L (Regular AutoHotkey) 81 | `#Include AppFactory.ahk` 82 | ##### AHK_H 83 | `#Include ..\Source\AppFactory.ahk` 84 | #### Initializing the library 85 | `factory := new AppFactory()` 86 | 87 | #### Adding GuiControls 88 | ##### Hotkey GuiControls 89 | These allow you to associate an end-user defined key sequence with a specific piece of code 90 | `factory.AddInputButton(, , )` 91 | ###### ControlName 92 | The unique name for this control - should ideally have no spaces. 93 | This will be used by other commands to refer to this control 94 | ###### Options 95 | Options for the GuiControl (To control position, size etc) 96 | Uses the same format as if you were doing a normal AHK [`Gui, Add`](https://www.autohotkey.com/docs/commands/Gui.htm#Add) command 97 | ###### Callback 98 | The function to call when the hotkey changes state (The user presses or releases the hotkey) 99 | The callback function is passed the state of the hotkey, eg: 100 | ``` 101 | factory.AddInputButton("HK1", "w200", Func("InputEvent")) 102 | ; ... 103 | InputEvent(state){ 104 | ; state will be 1 when the hotkey is pressed, 0 when the hotkey is released 105 | } 106 | ``` 107 | 108 | ##### Persistent GuiControls 109 | This can be thought of as an equivelent to AHK's [`Gui, Add`](https://www.autohotkey.com/docs/commands/Gui.htm#Add) command, except that the value of the GuiControl is remembered between runs of the script. 110 | `obj := factory.AddControl(, , [, , ])` 111 | ###### ControlName 112 | The unique name for this control - should ideally have no spaces. 113 | This will be used by other commands to refer to this control 114 | ###### ControlType 115 | One of the normal [AHK names for GuiControl types](https://www.autohotkey.com/docs/commands/Gui.htm#Add) 116 | ###### Options 117 | (Optional) Options for the GuiControl (To control position, size etc) 118 | Uses the same format as if you were doing a normal AHK `Gui, Add` command 119 | ###### Text 120 | (Optional) Performs the same function as the Text parameter of Gui, Add (Sets default value etc) 121 | ###### Callback 122 | (Optional) A Function Object that is to be called whenever the contents of the control changes. 123 | The callback function is passed the new value of the control, eg: 124 | ``` 125 | factory.AddControl("UserName", "Edit", "xm w200", "Default Value", Func("MyFunc")) 126 | ; ... 127 | MyFunc(value){ 128 | ; value holds the new value of the control 129 | } 130 | ``` 131 | ###### Return Value 132 | A reference to the GuiControl object is returned by this function (`obj` in the above example), which may optionally be stored in a variable. 133 | 134 | ###### Examples 135 | `factory.AddControl("UserName", "Edit", "xm w200")` Create a control called `UserName`, of type `Edit`, positioned against the left margin, with a width of 200px 136 | `factory.AddControl("UserName", "Edit", "xm w200", "Default Value")` As before, but with a default value of `Default Value` 137 | `factory.AddControl("UserName", "Edit", "xm w200", "Default Value", Func("MyFunc"))` As before, but when the user types something in the Edit box, as each character is typed, fire the function `MyFunc` and pass it the new value 138 | 139 | ###### Accessing the value of Persistent GuiControls via their objects 140 | The current value of a GuiControl can be retreived by calling `Get()` on the GuiControl object. 141 | There are two ways to get access to the GuiControl object 142 | 1. Via the optional returned object 143 | ``` 144 | obj := factory.AddControl(... 145 | currentValue := obj.Get() 146 | ``` 147 | 2. Via the `GuiControls` property of the factory object 148 | ``` 149 | factory.AddControl("MyControl", ... 150 | obj := factory.GuiControls.MyControl 151 | currentValue := obj.Get() 152 | ``` 153 | -------------------------------------------------------------------------------- /AppFactory_H/Source/BindModeThread.ahk: -------------------------------------------------------------------------------- 1 | #NoEnv 2 | /* 3 | Handles binding of the hotkeys for Bind Mode 4 | Runs as a separate thread to the main application, 5 | so that bind mode keys can be turned on and off quickly with Suspend 6 | */ 7 | /* 8 | #Persistent 9 | #NoTrayIcon 10 | #MaxHotkeysPerInterval 9999 11 | autoexecute_done := 1 12 | */ 13 | class _BindMapper { 14 | DetectionState := 0 15 | static IOClasses := {AHK_Common: 0, AHK_KBM_Input: 0, AHK_JoyBtn_Input: 0, AHK_JoyHat_Input: 0} 16 | __New(CallbackPtr){ 17 | this.Callback := ObjShare(CallbackPtr) 18 | ;this.Callback := CallbackPtr 19 | ; Instantiate each of the IOClasses specified in the IOClasses array 20 | for name, state in this.IOClasses { 21 | ; Instantiate an instance of a class that is a child class of this one. Thanks to HotkeyIt for this code! 22 | ; Replace each 0 in the array with an instance of the relevant class 23 | call:=this.base[name] 24 | this.IOClasses[name] := new call(this.Callback) 25 | ; debugging string 26 | if (i) 27 | names .= ", " 28 | names .= name 29 | i++ 30 | } 31 | if (i){ 32 | ;OutputDebug % "UCR| Bind Mode Thread loaded IOClasses: " names 33 | } else { 34 | OutputDebug % "UCR| Bind Mode Thread WARNING! Loaded No IOClasses!" 35 | } 36 | Suspend, On 37 | global InterfaceSetDetectionState := ObjShare(this.SetDetectionState.Bind(this)) 38 | } 39 | 40 | ; A request was received from the main thread to set the Dection state 41 | SetDetectionState(state, IOClassMappingsPtr){ 42 | if (state == this.DetectionState) 43 | return 44 | IOClassMappings := {} 45 | IOClassMappings := this.IndexedToAssoc(ObjShare(IOClassMappingsPtr)) 46 | for name, ret in IOClassMappings { 47 | ;OutputDebug % "UCR| BindModeThread Starting watcher " name " with return type " ret 48 | this.IOClasses[name].SetDetectionState(state, ret) 49 | } 50 | this.DetectionState := state 51 | } 52 | 53 | ; Converts an Indexed array of objects to an associative array 54 | ; If you pass an associative array via ObjShare, you cannot enumerate it 55 | ; So it is converted to an indexed array of objects, this converts it back. 56 | IndexedToAssoc(arr){ 57 | ret := {} 58 | Loop % arr.length(){ 59 | obj := arr[A_Index], ret[obj.k] := obj.v 60 | } 61 | return ret 62 | } 63 | 64 | ; ================================================================================================================== 65 | 66 | class AHK_Common { 67 | __New(callback){ 68 | this.Callback := callback 69 | } 70 | 71 | SetDetectionState(state, ReturnIOClass){ 72 | ;OutputDebug % "Turning Hotkeys " (state ? "On" : "Off") 73 | Suspend, % (state ? "Off", "On") 74 | } 75 | } 76 | 77 | ; ================================================================================================================== 78 | class AHK_KBM_Input { 79 | static IOClass := "AHK_KBM_Input" 80 | DebugMode := 2 81 | 82 | __New(callback){ 83 | this.Callback := callback 84 | this.CreateHotkeys() 85 | } 86 | 87 | SetDetectionState(state, ReturnIOClass){ 88 | ;this.ReturnIOClass := ( state ? ReturnIOClass : 0) 89 | this.ReturnIOClass := ReturnIOClass 90 | 91 | } 92 | 93 | ; Binds a key to every key on the keyboard and mouse 94 | ; Passes VK codes to GetKeyName() to obtain names for all keys 95 | ; List of VKs: https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx 96 | ; Keys are stored in the settings file by VK number, not by name. 97 | ; AHK returns non-standard names for some VKs, these are patched to Standard values 98 | ; Numpad Enter appears to have no VK, it is synonymous with Enter (VK0xD). Seeing as VKs 0xE to 0xF are Undefined by MSDN, we use 0xE for Numpad Enter. 99 | CreateHotkeys(){ 100 | static replacements := {33: "PgUp", 34: "PgDn", 35: "End", 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 45: "Insert", 46: "Delete"} 101 | static pfx := "$*" 102 | static updown := [{e: 1, s: ""}, {e: 0, s: " up"}] 103 | ; Cycle through all keys / mouse buttons 104 | Loop 256 { 105 | ; Get the key name 106 | i := A_Index 107 | code := Format("{:x}", i) 108 | if (ObjHasKey(replacements, i)){ 109 | n := replacements[i] 110 | } else { 111 | n := GetKeyName("vk" code) 112 | } 113 | if (n = "") 114 | continue 115 | ; Down event, then Up event 116 | Loop 2 { 117 | blk := this.DebugMode = 2 || (this.DebugMode = 1 && i <= 2) ? "~" : "" 118 | fn := this.InputEvent.Bind(this, updown[A_Index].e, i) 119 | hotkey, % pfx blk n updown[A_Index].s, % fn, % "On" 120 | } 121 | } 122 | i := 14, n := "NumpadEnter" ; Use 0xE for Nupad Enter 123 | Loop 2 { 124 | blk := this.DebugMode = 2 || (this.DebugMode = 1 && i <= 2) ? "~" : "" 125 | fn := this.InputEvent.Bind(this, updown[A_Index].e, i) 126 | hotkey, % pfx blk n updown[A_Index].s, % fn, % "On" 127 | } 128 | /* 129 | ; Cycle through all Joystick Buttons 130 | Loop 8 { 131 | j := A_Index 132 | Loop % this.JoystickCaps[j].btns { 133 | btn := A_Index 134 | n := j "Joy" A_Index 135 | fn := this._JoystickButtonDown.Bind(this, 1, 2, btn, j) 136 | hotkey, % pfx n, % fn, % "On" 137 | } 138 | } 139 | */ 140 | critical off 141 | } 142 | 143 | InputEvent(e, i){ 144 | ;tooltip % "code: " i ", e: " e 145 | this.Callback.Call(e, i, 0, this.ReturnIOClass) 146 | } 147 | } 148 | 149 | ; ================================================================================================================== 150 | class AHK_JoyBtn_Input { 151 | static IOClass := "AHK_JoyBtn_Input" 152 | DebugMode := 1 153 | JoystickCaps := [] 154 | 155 | __New(callback){ 156 | this.Callback := callback 157 | this.CreateHotkeys() 158 | } 159 | 160 | SetDetectionState(state, ReturnIOClass){ 161 | ;this.ReturnIOClass := ( state ? ReturnIOClass : 0) 162 | this.ReturnIOClass := ReturnIOClass 163 | } 164 | 165 | ; Binds a key to every key on the keyboard and mouse 166 | ; Passes VK codes to GetKeyName() to obtain names for all keys 167 | ; List of VKs: https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx 168 | ; Keys are stored in the settings file by VK number, not by name. 169 | ; AHK returns non-standard names for some VKs, these are patched to Standard values 170 | ; Numpad Enter appears to have no VK, it is synonymous with Enter (VK0xD). Seeing as VKs 0xE to 0xF are Undefined by MSDN, we use 0xE for Numpad Enter. 171 | CreateHotkeys(){ 172 | static updown := [{e: 1, s: ""}, {e: 0, s: " up"}] 173 | this.GetJoystickCaps() 174 | Loop 8 { 175 | j := A_Index 176 | Loop % this.JoystickCaps[j].btns { 177 | btn := A_Index 178 | n := j "Joy" A_Index 179 | fn := this.InputEvent.Bind(this, 1, btn, j) 180 | hotkey, % n, % fn, % "On" 181 | fn := this.InputEvent.Bind(this, 0, btn, j) 182 | hotkey, % n " up", % fn, % "On" 183 | } 184 | } 185 | } 186 | 187 | GetJoystickCaps(){ 188 | Loop 8 { 189 | cap := {} 190 | cap.btns := GetKeyState(A_Index "JoyButtons") 191 | this.JoystickCaps.push(cap) 192 | } 193 | } 194 | 195 | InputEvent(e, i, deviceid){ 196 | this.Callback.Call(e, i, deviceid, this.ReturnIOClass) 197 | } 198 | } 199 | 200 | ; ================================================================================================================== 201 | class AHK_JoyHat_Input { 202 | static IOClass := "AHK_JoyHat_Input" 203 | DebugMode := 1 204 | HatStrings := {} 205 | 206 | __New(callback){ 207 | this.Callback := callback 208 | Loop 8 { 209 | ji := GetKeyState(A_Index "JoyInfo") 210 | if (InStr(ji, "P")){ 211 | this.HatStrings[A_Index "JoyPov"] := {DeviceID: A_Index, State: -1} 212 | } 213 | } 214 | this.HatWatcherFn := this.HatWatcher.Bind(this) 215 | } 216 | 217 | 218 | SetDetectionState(state, ReturnIOClass){ 219 | ;this.ReturnIOClass := ( state ? ReturnIOClass : 0) 220 | this.ReturnIOClass := ReturnIOClass 221 | fn := this.HatWatcherFn 222 | t := state ? 10 : "Off" 223 | SetTimer, % fn, % t 224 | } 225 | 226 | HatWatcher(){ 227 | for bindstring, obj in this.HatStrings { 228 | state := GetKeyState(bindstring) 229 | if (obj.state == -1 && state != -1){ 230 | ; Press 231 | e := 1 232 | } else if (obj.state != -1 && state == -1){ 233 | ; Release 234 | e := 0 235 | } else { 236 | ; No Change / Bad values 237 | continue 238 | } 239 | i := state == -1 ? -1 : (round(state / 9000) + 1) 240 | DeviceID := obj.DeviceID 241 | obj.state := state 242 | this.Callback.Call(e, i, deviceid, this.ReturnIOClass) 243 | } 244 | } 245 | } 246 | } -------------------------------------------------------------------------------- /AppFactory/JSON.ahk: -------------------------------------------------------------------------------- 1 | /** 2 | * Lib: JSON.ahk 3 | * JSON lib for AutoHotkey. 4 | * Version: 5 | * v2.1.0 [updated 01/28/2016 (MM/DD/YYYY)] 6 | * License: 7 | * WTFPL [http://wtfpl.net/] 8 | * Requirements: 9 | * Latest version of AutoHotkey (v1.1+ or v2.0-a+) 10 | * Installation: 11 | * Use #Include JSON.ahk or copy into a function library folder and then 12 | * use #Include 13 | * Links: 14 | * GitHub: - https://github.com/cocobelgica/AutoHotkey-JSON 15 | * Forum Topic - http://goo.gl/r0zI8t 16 | * Email: - cocobelgica gmail com 17 | */ 18 | 19 | 20 | /** 21 | * Class: JSON 22 | * The JSON object contains methods for parsing JSON and converting values 23 | * to JSON. Callable - NO; Instantiable - YES; Subclassable - YES; 24 | * Nestable(via #Include) - NO. 25 | * Methods: 26 | * Load() - see relevant documentation before method definition header 27 | * Dump() - see relevant documentation before method definition header 28 | */ 29 | class JSON 30 | { 31 | /** 32 | * Method: Load 33 | * Parses a JSON string into an AHK value 34 | * Syntax: 35 | * value := JSON.Load( text [, reviver ] ) 36 | * Parameter(s): 37 | * value [retval] - parsed value 38 | * text [in, opt] - JSON formatted string 39 | * reviver [in, opt] - function object, similar to JavaScript's 40 | * JSON.parse() 'reviver' parameter 41 | */ 42 | class Load extends JSON.Functor 43 | { 44 | Call(self, text, reviver:="") 45 | { 46 | this.rev := IsObject(reviver) ? reviver : false 47 | this.keys := this.rev ? {} : false 48 | 49 | static q := Chr(34) 50 | , json_value := q . "{[01234567890-tfn" 51 | , json_value_or_array_closing := q . "{[]01234567890-tfn" 52 | , object_key_or_object_closing := q . "}" 53 | 54 | key := "" 55 | is_key := false 56 | root := {} 57 | stack := [root] 58 | next := json_value 59 | pos := 0 60 | 61 | while ((ch := SubStr(text, ++pos, 1)) != "") { 62 | if InStr(" `t`r`n", ch) 63 | continue 64 | if !InStr(next, ch, 1) 65 | this.ParseError(next, text, pos) 66 | 67 | holder := stack[1] 68 | is_array := holder.IsArray 69 | 70 | if InStr(",:", ch) { 71 | next := (is_key := !is_array && ch == ",") ? q : json_value 72 | 73 | } else if InStr("}]", ch) { 74 | ObjRemoveAt(stack, 1) 75 | next := stack[1]==root ? "" : stack[1].IsArray ? ",]" : ",}" 76 | 77 | } else { 78 | if InStr("{[", ch) { 79 | ; Check if Array() is overridden and if its return value has 80 | ; the 'IsArray' property. If so, Array() will be called normally, 81 | ; otherwise, use a custom base object for arrays 82 | static json_array := Func("Array").IsBuiltIn || ![].IsArray ? {IsArray: true} : 0 83 | 84 | ; sacrifice readability for minor(actually negligible) performance gain 85 | (ch == "{") 86 | ? ( is_key := true 87 | , value := {} 88 | , next := object_key_or_object_closing ) 89 | ; ch == "[" 90 | : ( value := json_array ? new json_array : [] 91 | , next := json_value_or_array_closing ) 92 | 93 | ObjInsertAt(stack, 1, value) 94 | 95 | if (this.keys) 96 | this.keys[value] := [] 97 | 98 | } else { 99 | if (ch == q) { 100 | i := pos 101 | while (i := InStr(text, q,, i+1)) { 102 | value := StrReplace(SubStr(text, pos+1, i-pos-1), "\\", "\u005c") 103 | 104 | static ss_end := A_AhkVersion<"2" ? 0 : -1 105 | if (SubStr(value, ss_end) != "\") 106 | break 107 | } 108 | 109 | if (!i) 110 | this.ParseError("'", text, pos) 111 | 112 | value := StrReplace(value, "\/", "/") 113 | , value := StrReplace(value, "\" . q, q) 114 | , value := StrReplace(value, "\b", "`b") 115 | , value := StrReplace(value, "\f", "`f") 116 | , value := StrReplace(value, "\n", "`n") 117 | , value := StrReplace(value, "\r", "`r") 118 | , value := StrReplace(value, "\t", "`t") 119 | 120 | pos := i ; update pos 121 | 122 | i := 0 123 | while (i := InStr(value, "\",, i+1)) { 124 | if !(SubStr(value, i+1, 1) == "u") 125 | this.ParseError("\", text, pos - StrLen(SubStr(value, i+1))) 126 | 127 | uffff := Abs("0x" . SubStr(value, i+2, 4)) 128 | if (A_IsUnicode || uffff < 0x100) 129 | value := SubStr(value, 1, i-1) . Chr(uffff) . SubStr(value, i+6) 130 | } 131 | 132 | if (is_key) { 133 | key := value, next := ":" 134 | continue 135 | } 136 | 137 | } else { 138 | value := SubStr(text, pos, i := RegExMatch(text, "[\]\},\s]|$",, pos)-pos) 139 | 140 | static number := "number" 141 | if value is %number% 142 | value += 0 143 | else if (value == "true" || value == "false") 144 | value := %value% + 0 145 | else if (value == "null") 146 | value := "" 147 | else 148 | ; we can do more here to pinpoint the actual culprit 149 | ; but that's just too much extra work. 150 | this.ParseError(next, text, pos, i) 151 | 152 | pos += i-1 153 | } 154 | 155 | next := holder==root ? "" : is_array ? ",]" : ",}" 156 | } ; If InStr("{[", ch) { ... } else 157 | 158 | is_array? key := ObjPush(holder, value) : holder[key] := value 159 | 160 | if (this.keys && this.keys.HasKey(holder)) 161 | this.keys[holder].Push(key) 162 | } 163 | 164 | } ; while ( ... ) 165 | 166 | return this.rev ? this.Walk(root, "") : root[""] 167 | } 168 | 169 | ParseError(expect, text, pos, len:=1) 170 | { 171 | static q := Chr(34) 172 | 173 | line := StrSplit(SubStr(text, 1, pos), "`n", "`r").Length() 174 | col := pos - InStr(text, "`n",, -(StrLen(text)-pos+1)) 175 | msg := Format("{1}`n`nLine:`t{2}`nCol:`t{3}`nChar:`t{4}" 176 | , (expect == "") ? "Extra data" 177 | : (expect == "'") ? "Unterminated string starting at" 178 | : (expect == "\") ? "Invalid \escape" 179 | : (expect == ":") ? "Expecting ':' delimiter" 180 | : (expect == q) ? "Expecting object key enclosed in double quotes" 181 | : (expect == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'" 182 | : (expect == ",}") ? "Expecting ',' delimiter or object closing '}'" 183 | : (expect == ",]") ? "Expecting ',' delimiter or array closing ']'" 184 | : InStr(expect, "]") ? "Expecting JSON value or array closing ']'" 185 | : "Expecting JSON value(string, number, true, false, null, object or array)" 186 | , line, col, pos) 187 | 188 | static offset := A_AhkVersion<"2" ? -3 : -4 189 | throw Exception(msg, offset, SubStr(text, pos, len)) 190 | } 191 | 192 | Walk(holder, key) 193 | { 194 | value := holder[key] 195 | if IsObject(value) 196 | for i, k in this.keys[value] 197 | value[k] := this.Walk.Call(this, value, k) ; bypass __Call 198 | 199 | return this.rev.Call(holder, key, value) 200 | } 201 | } 202 | 203 | /** 204 | * Method: Dump 205 | * Converts an AHK value into a JSON string 206 | * Syntax: 207 | * str := JSON.Dump( value [, replacer, space ] ) 208 | * Parameter(s): 209 | * str [retval] - JSON representation of an AHK value 210 | * value [in] - any value(object, string, number) 211 | * replacer [in, opt] - function object, similar to JavaScript's 212 | * JSON.stringify() 'replacer' parameter 213 | * space [in, opt] - similar to JavaScript's JSON.stringify() 214 | * 'space' parameter 215 | */ 216 | class Dump extends JSON.Functor 217 | { 218 | Call(self, value, replacer:="", space:="") 219 | { 220 | this.rep := IsObject(replacer) ? replacer : "" 221 | 222 | this.gap := "" 223 | if (space) { 224 | static integer := "integer" 225 | if space is %integer% 226 | Loop, % ((n := Abs(space))>10 ? 10 : n) 227 | this.gap .= " " 228 | else 229 | this.gap := SubStr(space, 1, 10) 230 | 231 | this.indent := "`n" 232 | } 233 | 234 | return this.Str({"": value}, "") 235 | } 236 | 237 | Str(holder, key) 238 | { 239 | value := holder[key] 240 | 241 | if (this.rep) 242 | value := this.rep.Call(holder, key, value) 243 | 244 | if IsObject(value) { 245 | ; Check object type, skip serialization for other object types such as 246 | ; ComObject, Func, BoundFunc, FileObject, RegExMatchObject, Property, etc. 247 | static type := A_AhkVersion<"2" ? "" : Func("Type") 248 | if (type ? type.Call(value) == "Object" : ObjGetCapacity(value) != "") { 249 | if (this.gap) { 250 | stepback := this.indent 251 | this.indent .= this.gap 252 | } 253 | 254 | is_array := value.IsArray 255 | ; Array() is not overridden, rollback to old method of 256 | ; identifying array-like objects. Due to the use of a for-loop 257 | ; sparse arrays such as '[1,,3]' are detected as objects({}). 258 | if (!is_array) { 259 | for i in value 260 | is_array := i == A_Index 261 | until !is_array 262 | } 263 | 264 | str := "" 265 | if (is_array) { 266 | Loop, % value.Length() { 267 | if (this.gap) 268 | str .= this.indent 269 | 270 | v := this.Str(value, A_Index) 271 | str .= (v != "") && value.HasKey(A_Index) ? v . "," : "null," 272 | } 273 | } else { 274 | colon := this.gap ? ": " : ":" 275 | for k in value { 276 | v := this.Str(value, k) 277 | if (v != "") { 278 | if (this.gap) 279 | str .= this.indent 280 | 281 | str .= this.Quote(k) . colon . v . "," 282 | } 283 | } 284 | } 285 | 286 | if (str != "") { 287 | str := RTrim(str, ",") 288 | if (this.gap) 289 | str .= stepback 290 | } 291 | 292 | if (this.gap) 293 | this.indent := stepback 294 | 295 | return is_array ? "[" . str . "]" : "{" . str . "}" 296 | } 297 | 298 | } else ; is_number ? value : "value" 299 | return ObjGetCapacity([value], 1)=="" ? value : this.Quote(value) 300 | } 301 | 302 | Quote(string) 303 | { 304 | static q := Chr(34) 305 | 306 | if (string != "") { 307 | string := StrReplace(string, "\", "\\") 308 | ; , string := StrReplace(string, "/", "\/") ; optional in ECMAScript 309 | , string := StrReplace(string, q, "\" . q) 310 | , string := StrReplace(string, "`b", "\b") 311 | , string := StrReplace(string, "`f", "\f") 312 | , string := StrReplace(string, "`n", "\n") 313 | , string := StrReplace(string, "`r", "\r") 314 | , string := StrReplace(string, "`t", "\t") 315 | 316 | static rx_escapable := A_AhkVersion<"2" ? "O)[^\x20-\x7e]" : "[^\x20-\x7e]" 317 | while RegExMatch(string, rx_escapable, m) 318 | string := StrReplace(string, m.Value, Format("\u{1:04x}", Ord(m.Value))) 319 | } 320 | 321 | return q . string . q 322 | } 323 | } 324 | 325 | /** 326 | * Property: Undefined 327 | * Proxy for 'undefined' type 328 | * Syntax: 329 | * undefined := JSON.Undefined 330 | * Remarks: 331 | * For use with reviver and replacer functions since AutoHotkey does not 332 | * have an 'undefined' type. Returning blank("") or 0 won't work since these 333 | * can't be distnguished from actual JSON values. This leaves us with objects. 334 | * The caller may return a non-serializable AHK objects such as ComObject, 335 | * Func, BoundFunc, FileObject, RegExMatchObject, and Property to mimic the 336 | * behavior of returning 'undefined' in JavaScript but for the sake of code 337 | * readability and convenience, it's better to do 'return JSON.Undefined'. 338 | * Internally, the property returns a ComObject with the variant type of VT_EMPTY. 339 | */ 340 | Undefined[] 341 | { 342 | get { 343 | static empty := {}, vt_empty := ComObject(0, &empty, 1) 344 | return vt_empty 345 | } 346 | } 347 | 348 | class Functor 349 | { 350 | __Call(method, args*) 351 | { 352 | ; When casting to Call(), use a new instance of the "function object" 353 | ; so as to avoid directly storing the properties(used across sub-methods) 354 | ; into the "function object" itself. 355 | if IsObject(method) 356 | return (new this).Call(method, args*) 357 | else if (method == "") 358 | return (new this).Call(args*) 359 | } 360 | } 361 | } -------------------------------------------------------------------------------- /AppFactory_H/Source/JSON.ahk: -------------------------------------------------------------------------------- 1 | /** 2 | * Lib: JSON.ahk 3 | * JSON lib for AutoHotkey. 4 | * Version: 5 | * v2.1.0 [updated 01/28/2016 (MM/DD/YYYY)] 6 | * License: 7 | * WTFPL [http://wtfpl.net/] 8 | * Requirements: 9 | * Latest version of AutoHotkey (v1.1+ or v2.0-a+) 10 | * Installation: 11 | * Use #Include JSON.ahk or copy into a function library folder and then 12 | * use #Include 13 | * Links: 14 | * GitHub: - https://github.com/cocobelgica/AutoHotkey-JSON 15 | * Forum Topic - http://goo.gl/r0zI8t 16 | * Email: - cocobelgica gmail com 17 | */ 18 | 19 | 20 | /** 21 | * Class: JSON 22 | * The JSON object contains methods for parsing JSON and converting values 23 | * to JSON. Callable - NO; Instantiable - YES; Subclassable - YES; 24 | * Nestable(via #Include) - NO. 25 | * Methods: 26 | * Load() - see relevant documentation before method definition header 27 | * Dump() - see relevant documentation before method definition header 28 | */ 29 | class JSON 30 | { 31 | /** 32 | * Method: Load 33 | * Parses a JSON string into an AHK value 34 | * Syntax: 35 | * value := JSON.Load( text [, reviver ] ) 36 | * Parameter(s): 37 | * value [retval] - parsed value 38 | * text [in, opt] - JSON formatted string 39 | * reviver [in, opt] - function object, similar to JavaScript's 40 | * JSON.parse() 'reviver' parameter 41 | */ 42 | class Load extends JSON.Functor 43 | { 44 | Call(self, text, reviver:="") 45 | { 46 | this.rev := IsObject(reviver) ? reviver : false 47 | this.keys := this.rev ? {} : false 48 | 49 | static q := Chr(34) 50 | , json_value := q . "{[01234567890-tfn" 51 | , json_value_or_array_closing := q . "{[]01234567890-tfn" 52 | , object_key_or_object_closing := q . "}" 53 | 54 | key := "" 55 | is_key := false 56 | root := {} 57 | stack := [root] 58 | next := json_value 59 | pos := 0 60 | 61 | while ((ch := SubStr(text, ++pos, 1)) != "") { 62 | if InStr(" `t`r`n", ch) 63 | continue 64 | if !InStr(next, ch, 1) 65 | this.ParseError(next, text, pos) 66 | 67 | holder := stack[1] 68 | is_array := holder.IsArray 69 | 70 | if InStr(",:", ch) { 71 | next := (is_key := !is_array && ch == ",") ? q : json_value 72 | 73 | } else if InStr("}]", ch) { 74 | ObjRemoveAt(stack, 1) 75 | next := stack[1]==root ? "" : stack[1].IsArray ? ",]" : ",}" 76 | 77 | } else { 78 | if InStr("{[", ch) { 79 | ; Check if Array() is overridden and if its return value has 80 | ; the 'IsArray' property. If so, Array() will be called normally, 81 | ; otherwise, use a custom base object for arrays 82 | static json_array := Func("Array").IsBuiltIn || ![].IsArray ? {IsArray: true} : 0 83 | 84 | ; sacrifice readability for minor(actually negligible) performance gain 85 | (ch == "{") 86 | ? ( is_key := true 87 | , value := {} 88 | , next := object_key_or_object_closing ) 89 | ; ch == "[" 90 | : ( value := json_array ? new json_array : [] 91 | , next := json_value_or_array_closing ) 92 | 93 | ObjInsertAt(stack, 1, value) 94 | 95 | if (this.keys) 96 | this.keys[value] := [] 97 | 98 | } else { 99 | if (ch == q) { 100 | i := pos 101 | while (i := InStr(text, q,, i+1)) { 102 | value := StrReplace(SubStr(text, pos+1, i-pos-1), "\\", "\u005c") 103 | 104 | static ss_end := A_AhkVersion<"2" ? 0 : -1 105 | if (SubStr(value, ss_end) != "\") 106 | break 107 | } 108 | 109 | if (!i) 110 | this.ParseError("'", text, pos) 111 | 112 | value := StrReplace(value, "\/", "/") 113 | , value := StrReplace(value, "\" . q, q) 114 | , value := StrReplace(value, "\b", "`b") 115 | , value := StrReplace(value, "\f", "`f") 116 | , value := StrReplace(value, "\n", "`n") 117 | , value := StrReplace(value, "\r", "`r") 118 | , value := StrReplace(value, "\t", "`t") 119 | 120 | pos := i ; update pos 121 | 122 | i := 0 123 | while (i := InStr(value, "\",, i+1)) { 124 | if !(SubStr(value, i+1, 1) == "u") 125 | this.ParseError("\", text, pos - StrLen(SubStr(value, i+1))) 126 | 127 | uffff := Abs("0x" . SubStr(value, i+2, 4)) 128 | if (A_IsUnicode || uffff < 0x100) 129 | value := SubStr(value, 1, i-1) . Chr(uffff) . SubStr(value, i+6) 130 | } 131 | 132 | if (is_key) { 133 | key := value, next := ":" 134 | continue 135 | } 136 | 137 | } else { 138 | value := SubStr(text, pos, i := RegExMatch(text, "[\]\},\s]|$",, pos)-pos) 139 | 140 | static number := "number" 141 | if value is %number% 142 | value += 0 143 | else if (value == "true" || value == "false") 144 | value := %value% + 0 145 | else if (value == "null") 146 | value := "" 147 | else 148 | ; we can do more here to pinpoint the actual culprit 149 | ; but that's just too much extra work. 150 | this.ParseError(next, text, pos, i) 151 | 152 | pos += i-1 153 | } 154 | 155 | next := holder==root ? "" : is_array ? ",]" : ",}" 156 | } ; If InStr("{[", ch) { ... } else 157 | 158 | is_array? key := ObjPush(holder, value) : holder[key] := value 159 | 160 | if (this.keys && this.keys.HasKey(holder)) 161 | this.keys[holder].Push(key) 162 | } 163 | 164 | } ; while ( ... ) 165 | 166 | return this.rev ? this.Walk(root, "") : root[""] 167 | } 168 | 169 | ParseError(expect, text, pos, len:=1) 170 | { 171 | static q := Chr(34) 172 | 173 | line := StrSplit(SubStr(text, 1, pos), "`n", "`r").Length() 174 | col := pos - InStr(text, "`n",, -(StrLen(text)-pos+1)) 175 | msg := Format("{1}`n`nLine:`t{2}`nCol:`t{3}`nChar:`t{4}" 176 | , (expect == "") ? "Extra data" 177 | : (expect == "'") ? "Unterminated string starting at" 178 | : (expect == "\") ? "Invalid \escape" 179 | : (expect == ":") ? "Expecting ':' delimiter" 180 | : (expect == q) ? "Expecting object key enclosed in double quotes" 181 | : (expect == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'" 182 | : (expect == ",}") ? "Expecting ',' delimiter or object closing '}'" 183 | : (expect == ",]") ? "Expecting ',' delimiter or array closing ']'" 184 | : InStr(expect, "]") ? "Expecting JSON value or array closing ']'" 185 | : "Expecting JSON value(string, number, true, false, null, object or array)" 186 | , line, col, pos) 187 | 188 | static offset := A_AhkVersion<"2" ? -3 : -4 189 | throw Exception(msg, offset, SubStr(text, pos, len)) 190 | } 191 | 192 | Walk(holder, key) 193 | { 194 | value := holder[key] 195 | if IsObject(value) 196 | for i, k in this.keys[value] 197 | value[k] := this.Walk.Call(this, value, k) ; bypass __Call 198 | 199 | return this.rev.Call(holder, key, value) 200 | } 201 | } 202 | 203 | /** 204 | * Method: Dump 205 | * Converts an AHK value into a JSON string 206 | * Syntax: 207 | * str := JSON.Dump( value [, replacer, space ] ) 208 | * Parameter(s): 209 | * str [retval] - JSON representation of an AHK value 210 | * value [in] - any value(object, string, number) 211 | * replacer [in, opt] - function object, similar to JavaScript's 212 | * JSON.stringify() 'replacer' parameter 213 | * space [in, opt] - similar to JavaScript's JSON.stringify() 214 | * 'space' parameter 215 | */ 216 | class Dump extends JSON.Functor 217 | { 218 | Call(self, value, replacer:="", space:="") 219 | { 220 | this.rep := IsObject(replacer) ? replacer : "" 221 | 222 | this.gap := "" 223 | if (space) { 224 | static integer := "integer" 225 | if space is %integer% 226 | Loop, % ((n := Abs(space))>10 ? 10 : n) 227 | this.gap .= " " 228 | else 229 | this.gap := SubStr(space, 1, 10) 230 | 231 | this.indent := "`n" 232 | } 233 | 234 | return this.Str({"": value}, "") 235 | } 236 | 237 | Str(holder, key) 238 | { 239 | value := holder[key] 240 | 241 | if (this.rep) 242 | value := this.rep.Call(holder, key, value) 243 | 244 | if IsObject(value) { 245 | ; Check object type, skip serialization for other object types such as 246 | ; ComObject, Func, BoundFunc, FileObject, RegExMatchObject, Property, etc. 247 | static type := A_AhkVersion<"2" ? "" : Func("Type") 248 | if (type ? type.Call(value) == "Object" : ObjGetCapacity(value) != "") { 249 | if (this.gap) { 250 | stepback := this.indent 251 | this.indent .= this.gap 252 | } 253 | 254 | is_array := value.IsArray 255 | ; Array() is not overridden, rollback to old method of 256 | ; identifying array-like objects. Due to the use of a for-loop 257 | ; sparse arrays such as '[1,,3]' are detected as objects({}). 258 | if (!is_array) { 259 | for i in value 260 | is_array := i == A_Index 261 | until !is_array 262 | } 263 | 264 | str := "" 265 | if (is_array) { 266 | Loop, % value.Length() { 267 | if (this.gap) 268 | str .= this.indent 269 | 270 | v := this.Str(value, A_Index) 271 | str .= (v != "") && value.HasKey(A_Index) ? v . "," : "null," 272 | } 273 | } else { 274 | colon := this.gap ? ": " : ":" 275 | for k in value { 276 | v := this.Str(value, k) 277 | if (v != "") { 278 | if (this.gap) 279 | str .= this.indent 280 | 281 | str .= this.Quote(k) . colon . v . "," 282 | } 283 | } 284 | } 285 | 286 | if (str != "") { 287 | str := RTrim(str, ",") 288 | if (this.gap) 289 | str .= stepback 290 | } 291 | 292 | if (this.gap) 293 | this.indent := stepback 294 | 295 | return is_array ? "[" . str . "]" : "{" . str . "}" 296 | } 297 | 298 | } else ; is_number ? value : "value" 299 | return ObjGetCapacity([value], 1)=="" ? value : this.Quote(value) 300 | } 301 | 302 | Quote(string) 303 | { 304 | static q := Chr(34) 305 | 306 | if (string != "") { 307 | string := StrReplace(string, "\", "\\") 308 | ; , string := StrReplace(string, "/", "\/") ; optional in ECMAScript 309 | , string := StrReplace(string, q, "\" . q) 310 | , string := StrReplace(string, "`b", "\b") 311 | , string := StrReplace(string, "`f", "\f") 312 | , string := StrReplace(string, "`n", "\n") 313 | , string := StrReplace(string, "`r", "\r") 314 | , string := StrReplace(string, "`t", "\t") 315 | 316 | static rx_escapable := A_AhkVersion<"2" ? "O)[^\x20-\x7e]" : "[^\x20-\x7e]" 317 | while RegExMatch(string, rx_escapable, m) 318 | string := StrReplace(string, m.Value, Format("\u{1:04x}", Ord(m.Value))) 319 | } 320 | 321 | return q . string . q 322 | } 323 | } 324 | 325 | /** 326 | * Property: Undefined 327 | * Proxy for 'undefined' type 328 | * Syntax: 329 | * undefined := JSON.Undefined 330 | * Remarks: 331 | * For use with reviver and replacer functions since AutoHotkey does not 332 | * have an 'undefined' type. Returning blank("") or 0 won't work since these 333 | * can't be distnguished from actual JSON values. This leaves us with objects. 334 | * The caller may return a non-serializable AHK objects such as ComObject, 335 | * Func, BoundFunc, FileObject, RegExMatchObject, and Property to mimic the 336 | * behavior of returning 'undefined' in JavaScript but for the sake of code 337 | * readability and convenience, it's better to do 'return JSON.Undefined'. 338 | * Internally, the property returns a ComObject with the variant type of VT_EMPTY. 339 | */ 340 | Undefined[] 341 | { 342 | get { 343 | static empty := {}, vt_empty := ComObject(0, &empty, 1) 344 | return vt_empty 345 | } 346 | } 347 | 348 | class Functor 349 | { 350 | __Call(method, args*) 351 | { 352 | ; When casting to Call(), use a new instance of the "function object" 353 | ; so as to avoid directly storing the properties(used across sub-methods) 354 | ; into the "function object" itself. 355 | if IsObject(method) 356 | return (new this).Call(method, args*) 357 | else if (method == "") 358 | return (new this).Call(args*) 359 | } 360 | } 361 | } -------------------------------------------------------------------------------- /AppFactory/InputThread.ahk: -------------------------------------------------------------------------------- 1 | ; Can use #Include %A_LineFile%\..\other.ahk to include in same folder 2 | Class _InputThread { 3 | static IOClasses := {AHK_KBM_Input: 0, AHK_JoyBtn_Input: 0, AHK_JoyHat_Input: 0} 4 | DetectionState := 0 5 | UpdateBindingQueue := [] ; An array of bindings waiting to be updated. 6 | UpdatingBindings := 0 7 | ControlMappings := {} 8 | 9 | __New(Callback){ 10 | this.Callback := Callback 11 | names := "" 12 | i := 0 13 | ; Instantiate each of the IOClasses specified in the IOClasses array 14 | for name, state in this.IOClasses { 15 | ; Instantiate an instance of a class that is a child class of this one. Thanks to HotkeyIt for this code! 16 | ; Replace each 0 in the array with an instance of the relevant class 17 | call:=this.base[name] 18 | this.IOClasses[name] := new call(this.Callback) 19 | ; debugging string 20 | if (i) 21 | names .= ", " 22 | names .= name 23 | i++ 24 | } 25 | if (i){ 26 | ; OutputDebug % "UCR| Input Thread loaded IOClasses: " names 27 | } else { 28 | OutputDebug % "UCR| Input Thread WARNING! Loaded No IOClasses!" 29 | } 30 | 31 | ; Unreachable dummy label for hotkeys to bind to to clear binding 32 | if(0){ 33 | UCR_INPUTHREAD_DUMMY_LABEL: 34 | return 35 | } 36 | } 37 | 38 | UpdateBinding(ControlGUID, bo){ 39 | iom := this.ControlMappings[ControlGuid] 40 | if (this.ControlMappings.HasKey(ControlGuid) && iom != bo.IOClass){ 41 | this.IOClasses[iom].RemoveBinding(ControlGUID) 42 | } 43 | this.ControlMappings[ControlGuid] := bo.IOClass 44 | OutputDebug % "UCR| Updating binding for ControlGUID " ControlGUID ", IOClass " bo.IOClass 45 | ; Direct the request to the appropriate IOClass that handles it 46 | this.IOClasses[bo.IOClass].UpdateBinding(ControlGUID, bo) 47 | } 48 | 49 | ;~ _SetDetectionState(state){ 50 | SetDetectionState(state){ 51 | OutputDebug % "UCR| InputThread: Hotkey detection " (state ? "On" : "Off") 52 | if (state == this.DetectionState) 53 | return 54 | this.DetectionState := state 55 | for name, cls in this.IOClasses { 56 | cls.SetDetectionState(state) 57 | } 58 | } 59 | 60 | ; Listens for Keyboard and Mouse input using the AHK Hotkey command 61 | class AHK_KBM_Input { 62 | DetectionState := 0 63 | _AHKBindings := {} 64 | 65 | __New(callback){ 66 | this.callback := callback 67 | ;~ Suspend, On ; Start with detection off, even if we are passed bindings 68 | } 69 | 70 | UpdateBinding(ControlGUID, bo){ 71 | hotkey, If, !_AppFactoryBindMode 72 | this.RemoveBinding(ControlGUID) 73 | if (bo.Binding[1]){ 74 | keyname := "$" this.BuildHotkeyString(bo) 75 | fn := this.KeyEvent.Bind(this, ControlGUID, 1) 76 | hotkey, % keyname, % fn, On 77 | fn := this.KeyEvent.Bind(this, ControlGUID, 0) 78 | hotkey, % keyname " up", % fn, On 79 | ;OutputDebug % "UCR| AHK_KBM_Input Added hotkey " keyname " for ControlGUID " ControlGUID 80 | this._AHKBindings[ControlGUID] := keyname 81 | } 82 | } 83 | 84 | SetDetectionState(state){ 85 | ; Are we already in the requested state? 86 | ; This code is rigged so that either AHK_KBM_Input or AHK_JoyBtn_Input or both will not clash... 87 | ; ... As long as all are turned on or off together, you won't get weird results. 88 | ;~ if (A_IsSuspended == state){ 89 | ;~ ;OutputDebug % "UCR| Thread: AHK_KBM_Input IOClass turning Hotkey detection " (state ? "On" : "Off") 90 | ;~ Suspend, % (state ? "Off" : "On") 91 | ;~ } 92 | this.DetectionState := state 93 | } 94 | 95 | RemoveBinding(ControlGUID){ 96 | keyname := this._AHKBindings[ControlGUID] 97 | if (keyname){ 98 | ;OutputDebug % "UCR| AHK_KBM_Input Removing hotkey " keyname " for ControlGUID " ControlGUID 99 | hotkey, % keyname, UCR_INPUTHREAD_DUMMY_LABEL 100 | hotkey, % keyname, Off 101 | hotkey, % keyname " up", UCR_INPUTHREAD_DUMMY_LABEL 102 | hotkey, % keyname " up", Off 103 | this._AHKBindings.Delete(ControlGUID) 104 | } 105 | } 106 | 107 | KeyEvent(ControlGUID, e){ 108 | ;OutputDebug % "UCR| AHK_KBM_Input Key event for GuiControl " ControlGUID 109 | fn := this.InputEvent.Bind(this, ControlGUID, e) 110 | SetTimer, % fn, -0 111 | } 112 | 113 | InputEvent(ControlGUID, state){ 114 | OutputDebug % "AHK_KBM_Input Event: " state 115 | this.Callback.Call(ControlGUID, state) 116 | } 117 | 118 | ; Builds an AHK hotkey string (eg ~^a) from a BindObject 119 | BuildHotkeyString(bo){ 120 | if (!bo.Binding.Length()) 121 | return "" 122 | str := "" 123 | if (bo.BindOptions.Wild) 124 | str .= "*" 125 | if (!bo.BindOptions.Block) 126 | str .= "~" 127 | max := bo.Binding.Length() 128 | Loop % max { 129 | key := bo.Binding[A_Index] 130 | if (A_Index = max){ 131 | islast := 1 132 | nextkey := 0 133 | } else { 134 | islast := 0 135 | nextkey := bo[A_Index+1] 136 | } 137 | if (this.IsModifier(key) && (max > A_Index)){ 138 | str .= this.RenderModifier(key) 139 | } else { 140 | str .= this.BuildKeyName(key) 141 | } 142 | } 143 | return str 144 | } 145 | 146 | ; === COMMON WITH IOCLASS. MOVE TO INCLUDE ===== 147 | static _Modifiers := ({91: {s: "#", v: "<"},92: {s: "#", v: ">"} 148 | ,160: {s: "+", v: "<"},161: {s: "+", v: ">"} 149 | ,162: {s: "^", v: "<"},163: {s: "^", v: ">"} 150 | ,164: {s: "!", v: "<"},165: {s: "!", v: ">"}}) 151 | 152 | ; Builds the AHK key name 153 | BuildKeyName(code){ 154 | static replacements := {33: "PgUp", 34: "PgDn", 35: "End", 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 45: "Insert", 46: "Delete"} 155 | static additions := {14: "NumpadEnter"} 156 | if (ObjHasKey(replacements, code)){ 157 | return replacements[code] 158 | } else if (ObjHasKey(additions, code)){ 159 | return additions[code] 160 | } else { 161 | return GetKeyName("vk" Format("{:x}", code)) 162 | } 163 | } 164 | 165 | ; Returns true if this Button is a modifier key on the keyboard 166 | IsModifier(code){ 167 | return ObjHasKey(this._Modifiers, code) 168 | } 169 | 170 | ; Renders the keycode of a Modifier to it's AHK Hotkey symbol (eg 162 for LCTRL to ^) 171 | RenderModifier(code){ 172 | return this._Modifiers[code].s 173 | } 174 | ; ================= END MOVE TO INCLUDE ====================== 175 | } 176 | 177 | ; Listens for Joystick Button input using AHK's Hotkey command 178 | ; Joystick button Hotkeys in AHK immediately fire the up event after the down event... 179 | ; ... so up events are emulated up using AHK's GetKeyState() function 180 | class AHK_JoyBtn_Input { 181 | HeldButtons := {} 182 | TimerWanted := 0 ; Whether or not we WANT to run the ButtonTimer (NOT if it is actually running!) 183 | TimerRunning := 0 184 | DetectionState := 0 ; Whether or not we are allowed to have hotkeys or be running the timer 185 | 186 | __New(Callback){ 187 | this.Callback := Callback 188 | this.TimerFn := this.ButtonWatcher.Bind(this) 189 | ;~ Suspend, On ; Start with detection off, even if we are passed bindings 190 | } 191 | 192 | UpdateBinding(ControlGUID, bo){ 193 | this.RemoveBinding(ControlGUID) 194 | if (bo.Binding[1]){ 195 | keyname := this.BuildHotkeyString(bo) 196 | fn := this.KeyEvent.Bind(this, ControlGUID, 1) 197 | if (GetKeyState(bo.DeviceID "JoyAxes")) 198 | try { 199 | hotkey, % keyname, % fn, On 200 | } 201 | else 202 | OutputDebug % "UCR| Warning! AHK_JoyBtn_Input did not declare hotkey " keyname " because the stick is disconnected" 203 | ;OutputDebug % "UCR| AHK_JoyBtn_Input Added hotkey " keyname " for ControlGUID " ControlGUID 204 | this._AHKBindings[ControlGUID] := keyname 205 | } 206 | } 207 | 208 | SetDetectionState(state){ 209 | this.DetectionState := state 210 | this.ProcessTimerState() 211 | } 212 | 213 | RemoveBinding(ControlGUID){ 214 | keyname := this._AHKBindings[ControlGUID] 215 | if (keyname){ 216 | ;OutputDebug % "UCR| AHK_JoyBtn_Input Removing hotkey " keyname " for ControlGUID " ControlGUID 217 | try{ 218 | hotkey, % keyname, UCR_INPUTHREAD_DUMMY_LABEL 219 | } 220 | try{ 221 | hotkey, % keyname, Off 222 | } 223 | this._AHKBindings.Delete(ControlGUID) 224 | } 225 | ;this._CurrentBinding := 0 226 | } 227 | 228 | KeyEvent(ControlGUID, e){ 229 | ; ToDo: Parent will not exist in thread! 230 | 231 | ;OutputDebug % "UCR| AHK_JoyBtn_Input Key event " e " for GuiControl " ControlGUID 232 | ;this.Callback.Call(ControlGUID, e) 233 | fn := this.InputEvent.Bind(this, ControlGUID, e) 234 | SetTimer, % fn, -0 235 | 236 | this.HeldButtons[this._AHKBindings[ControlGUID]] := ControlGUID 237 | if (!this.TimerWanted){ 238 | this.TimerWanted := 1 239 | this.ProcessTimerState() 240 | } 241 | } 242 | 243 | InputEvent(ControlGUID, state){ 244 | this.Callback.Call(ControlGUID, state) 245 | } 246 | 247 | ButtonWatcher(){ 248 | for bindstring, ControlGUID in this.HeldButtons { 249 | if (!GetKeyState(bindstring)){ 250 | this.HeldButtons.Delete(bindstring) 251 | ;OutputDebug % "UCR| AHK_JoyBtn_Input Key event 0 for GuiControl " ControlGUID 252 | ;this.Callback.Call(ControlGUID, 0) 253 | fn := this.InputEvent.Bind(this, ControlGUID, 0) 254 | SetTimer, % fn, -0 255 | if (IsEmptyAssoc(this.HeldButtons)){ 256 | this.TimerWanted := 0 257 | this.ProcessTimerState() 258 | return 259 | } 260 | } 261 | } 262 | } 263 | 264 | ProcessTimerState(){ 265 | fn := this.TimerFn 266 | if (this.TimerWanted && this.DetectionState && !this.TimerRunning){ 267 | SetTimer, % fn, 10 268 | this.TimerRunning := 1 269 | ;OutputDebug % "UCR| AHK_JoyBtn_Input Started ButtonWatcher " ControlGUID 270 | } else if ((!this.TimerWanted || !this.DetectionState) && this.TimerRunning){ 271 | SetTimer, % fn, Off 272 | this.TimerRunning := 0 273 | ;OutputDebug % "UCR| AHK_JoyBtn_Input Stopped ButtonWatcher " ControlGUID 274 | } 275 | } 276 | 277 | BuildHotkeyString(bo){ 278 | return bo.Deviceid "Joy" bo.Binding[1] 279 | } 280 | } 281 | 282 | ; Listens for Joystick Hat input using AHK's GetKeyState() function 283 | class AHK_JoyHat_Input { 284 | ; Indexed by GetKeyState string (eg "1JoyPOV") 285 | ; The HatWatcher timer is active while this array has items. 286 | ; Contains an array of objects whose keys are the GUIDs of GuiControls mapped to that POV 287 | ; Properties of those keys are the direction of the mapping and the state of the binding 288 | HatBindings := {} 289 | 290 | ; GUID-Indexed array of sticks + directions that each GUIControl is mapped to, plus it's current state 291 | ControlMappings := {} 292 | 293 | ; Which cardinal directions are pressed for each of the 8 compass directions, plus centre 294 | ; Order is U, R, D, L 295 | static PovMap := {-1: [0,0,0,0], 1: [1,0,0,0], 2: [1,1,0,0] , 3: [0,1,0,0], 4: [0,1,1,0], 5: [0,0,1,0], 6: [0,0,1,1], 7: [0,0,0,1], 8: [1,0,0,1]} 296 | 297 | TimerRunning := 0 298 | TimerWanted := 0 299 | ConnectedSticks := [0,0,0,0,0,0,0,0] 300 | 301 | __New(Callback){ 302 | this.Callback := Callback 303 | 304 | this.TimerFn := this.HatWatcher.Bind(this) 305 | } 306 | 307 | ; Request from main thread to update binding 308 | UpdateBinding(ControlGUID, bo){ 309 | ;OutputDebug % "UCR| AHK_JoyHat_Input " (bo.Binding[1] ? "Update" : "Remove" ) " Hat Binding - Device: " bo.DeviceID ", Direction: " bo.Binding[1] 310 | this._UpdateArrays(ControlGUID, bo) 311 | this.TimerWanted := !IsEmptyAssoc(this.ControlMappings) 312 | this.ProcessTimerState() 313 | } 314 | 315 | SetDetectionState(state){ 316 | this.DetectionState := state 317 | this.ProcessTimerState() 318 | } 319 | 320 | ProcessTimerState(){ 321 | fn := this.TimerFn 322 | if (this.TimerWanted && this.DetectionState && !this.TimerRunning){ 323 | ; Pre-cache connected sticks, as polling disconnected sticks takes lots of CPU 324 | Loop 8 { 325 | this.ConnectedSticks[A_Index] := GetKeyState(A_Index "JoyInfo") 326 | } 327 | SetTimer, % fn, 10 328 | this.TimerRunning := 1 329 | ;OutputDebug % "UCR| AHK_JoyHat_Input Started HatWatcher" 330 | } else if ((!this.TimerWanted || !this.DetectionState) && this.TimerRunning){ 331 | SetTimer, % fn, Off 332 | this.TimerRunning := 0 333 | ;OutputDebug % "UCR| AHK_JoyHat_Input Stopped HatWatcher" 334 | } 335 | } 336 | 337 | ; Updates the arrays which drive hat detection 338 | _UpdateArrays(ControlGUID, bo := 0){ 339 | if (ObjHasKey(this.ControlMappings, ControlGUID)){ 340 | ; GuiControl already has binding 341 | bindstring := this.ControlMappings[ControlGUID].bindstring 342 | this.HatBindings[bindstring].Delete(ControlGUID) 343 | this.ControlMappings.Delete(ControlGUID) 344 | if (IsEmptyAssoc(this.HatBindings[bindstring])){ 345 | this.HatBindings.Delete(bindstring) 346 | ;OutputDebug % "UCR| AHK_JoyHat_Input Removing Hat Bindstring " bindstring 347 | } 348 | } 349 | if (bo != 0 && bo.Binding[1]){ 350 | ; there is a new binding 351 | bindstring := bo.DeviceID "JoyPOV" 352 | if (!ObjHasKey(this.HatBindings, bindstring)){ 353 | this.HatBindings[bindstring] := {} 354 | ;OutputDebug % "UCR| AHK_JoyHat_Input Adding Hat Bindstring " bindstring 355 | } 356 | this.HatBindings[bindstring, ControlGUID] := {dir: bo.Binding[1], state: 0} 357 | this.ControlMappings[ControlGUID] := {bindstring: bindstring} 358 | } 359 | } 360 | 361 | ; Called on a timer when we are trying to detect hats 362 | HatWatcher(){ 363 | for bindstring, bindings in this.HatBindings { 364 | if (!this.ConnectedSticks[SubStr(bindstring, 1, 1)]){ 365 | ; Do not poll unconnected sticks, it consumes a lot of cpu 366 | continue 367 | } 368 | state := GetKeyState(bindstring) 369 | state := (state = -1 ? -1 : round(state / 4500) + 1) 370 | for ControlGUID, obj in bindings { 371 | new_state := (this.PovMap[state, obj.dir] == 1) 372 | if (obj.state != new_state){ 373 | obj.state := new_state 374 | ;OutputDebug % "UCR| InputThread: AHK_JoyHat_Input Direction " obj.dir " state " new_state " calling ControlGUID " ControlGUID 375 | ; Use the thread-safe object to tell the main thread that the hat direction changed state 376 | ;this.Callback.Call(ControlGUID, new_state) 377 | fn := this.InputEvent.Bind(this, ControlGUID, new_state) 378 | SetTimer, % fn, -0 379 | } 380 | } 381 | } 382 | } 383 | 384 | InputEvent(ControlGUID, state){ 385 | this.Callback.Call(ControlGUID, state) 386 | } 387 | } 388 | } 389 | 390 | ; Is an associative array empty? 391 | IsEmptyAssoc(assoc){ 392 | return !assoc._NewEnum()[k, v] 393 | } -------------------------------------------------------------------------------- /AppFactory/AppFactory.ahk: -------------------------------------------------------------------------------- 1 | #include %A_LineFile%\..\JSON.ahk 2 | #include %A_LineFile%\..\BindModeThread.ahk 3 | #include %A_LineFile%\..\InputThread.ahk 4 | 5 | ; "hotkey, if" needs to have actual #if blocks to match to, so declare empty ones 6 | #If _AppFactoryBindMode 7 | #If !_AppFactoryBindMode 8 | #If 9 | 10 | _AppFactoryBindMode := 0 11 | 12 | Class AppFactory { 13 | InputThread := 0 14 | IOControls := {} 15 | GuiControls := {} 16 | Settings := {} 17 | 18 | ; ====================== PUBLIC METHODS. USER SCRIPTS SHOULD ONLY CALL THESE ======================== 19 | AddInputButton(guid, options, callback){ 20 | this.IOControls[guid] := new this._IOControl(this, guid, options, callback) 21 | this.IOControls[guid].SetBinding(this.Settings.IOControls[guid]) 22 | } 23 | 24 | AddControl(guid, ctrltype, options := "", default := "", callback := 0){ 25 | this.GuiControls[guid] := new this._GuiControl(this, guid, ctrltype, options, default, callback) 26 | if (this.Settings.GuiControls.Haskey(guid)){ 27 | this.GuiControls[guid].SetValue(this.Settings.GuiControls[guid]) 28 | } else { 29 | if (this.GuiControls[guid].IsListType){ 30 | d := RegExMatch(default, "(.*)\|\|", out) 31 | default := out1 32 | } 33 | this.GuiControls[guid].SetValue(default) 34 | } 35 | 36 | } 37 | 38 | ; ====================== PRIVATE METHODS. USER SCRIPTS SHOULD NOT CALL THESE ======================== 39 | __New(hwnd := 0){ 40 | this._SettingsFile := RegExReplace(A_ScriptName, ".ahk|.exe", ".ini") 41 | 42 | this.InitBindMode() 43 | this.InitInputThread() 44 | 45 | if (hwnd == 0) 46 | Gui, +Hwndhwnd 47 | this.hwnd := hwnd 48 | 49 | FileRead, j, % this._SettingsFile 50 | if (j == ""){ 51 | j := {IOControls: {}, GuiControls: {}} 52 | } else { 53 | j := JSON.Load(j) 54 | } 55 | this.Settings := j 56 | this.InputThread.SetDetectionState(1) 57 | } 58 | 59 | ; When bind mode ends, the GuiControl will call this method to request that the setting be saved 60 | _BindingChanged(ControlGuid, bo){ 61 | this.Settings.IOControls[ControlGuid] := bo 62 | this._SaveSettings() 63 | } 64 | 65 | _GuiControlChanged(ControlGuid, value){ 66 | this.Settings.GuiControls[ControlGuid] := value 67 | this._SaveSettings() 68 | } 69 | 70 | _SaveSettings(){ 71 | FileDelete, % this._SettingsFile 72 | FileAppend, % JSON.Dump(this.Settings, ,true), % this._SettingsFile 73 | } 74 | 75 | ; ============================================================================================ 76 | ; ==================================== GUICONTROLS =========================================== 77 | ; ============================================================================================ 78 | class _GuiControl { 79 | static _ListTypes := {ListBox: 1, DDL: 1, DropDownList: 1, ComboBox: 1, Tab: 1, Tab2: 1, Tab3: 1} 80 | _Value := "" 81 | 82 | Get(){ 83 | return this._Value 84 | } 85 | 86 | __New(parent, guid, ctrltype, options, default, callback){ 87 | this.id := guid 88 | this.parent := parent 89 | this.Callback := callback 90 | this.Default := default 91 | 92 | if (ObjHasKey(this._ListTypes, ctrltype)){ 93 | this.IsListType := 1 94 | ; Detect if this List Type uses AltSubmit 95 | if (InStr(options, "altsubmit")) 96 | this.IsAltSubmitType := 1 97 | else 98 | this.IsAltSubmitType := 0 99 | } else { 100 | this.IsListType := 0 101 | this.IsAltSubmitType := 0 102 | } 103 | 104 | Gui, % this.parent.hwnd ":Add", % ctrltype, % "hwndhwnd " options, % default 105 | this.hwnd := hwnd 106 | fn := this.ControlChanged.Bind(this) 107 | this.ChangeValueFn := fn 108 | this._SetGLabel(1) 109 | 110 | return this 111 | } 112 | 113 | SetControlState(value){ 114 | this._SetGlabel(0) ; Turn off g-label to avoid triggering save 115 | cmd := "" 116 | if (this.IsListType){ 117 | cmd := (this.IsAltSubmitType ? "choose" : "choosestring") 118 | } 119 | GuiControl, % this.parent.hwnd ":" cmd, % this.hwnd, % value 120 | this._SetGlabel(1) ; Turn g-label back on 121 | } 122 | 123 | ; Turns on or off the g-label for the GuiControl 124 | ; This is needed to work around not being able to programmatically set GuiControl without triggering g-label 125 | _SetGlabel(state){ 126 | if (state){ 127 | fn := this.ChangeValueFn 128 | GuiControl, % this.parent.hwnd ":+g", % this.hwnd, % fn 129 | } else { 130 | GuiControl, % this.parent.hwnd ":-g", % this.hwnd 131 | } 132 | } 133 | 134 | ; User interacted with GuiControl 135 | ControlChanged(){ 136 | GuiControlGet, value, % this.parent.hwnd ":" , % this.hwnd 137 | this._Value := value 138 | if (this.Callback != 0){ 139 | this.Callback.call(value) 140 | } 141 | this.parent._GuiControlChanged(this.id, value) 142 | } 143 | 144 | ; Called on load of settings 145 | SetValue(value){ 146 | this._Value := value 147 | this.Callback.call(value) 148 | this.SetControlState(value) 149 | } 150 | } 151 | 152 | ; ============================================================================================ 153 | ; ==================================== IOCONTROLS ============================================ 154 | ; ============================================================================================ 155 | class _IOControl { 156 | guid := 0 ; The unique ID/Name for this IOControl 157 | Callback := 0 ; Holds the user's callback for this IOControl 158 | BindObject := 0 ; Holds the BindObject describing the current binding 159 | State := 0 ; The State of the input. Only really used for Repeat Suppression 160 | 161 | static _Modifiers := ({91: {s: "#", v: "<"},92: {s: "#", v: ">"} 162 | ,160: {s: "+", v: "<"},161: {s: "+", v: ">"} 163 | ,162: {s: "^", v: "<"},163: {s: "^", v: ">"} 164 | ,164: {s: "!", v: "<"},165: {s: "!", v: ">"}}) 165 | 166 | __New(parent, guid, options, callback){ 167 | this.id := guid 168 | this.parent := parent 169 | this.Callback := callback 170 | this.BindObject := new this.parent._BindObject() 171 | Gui, % this.parent.hwnd ":Add", Button, % "hwndhReadout " options , Select an Input Button 172 | this.hReadout := hReadout 173 | fn := this.OpenMenu.Bind(this) 174 | GuiControl, % this.parent.hwnd ":+g", % hReadout, % fn 175 | 176 | fn := this.IOControlChoiceMade.Bind(this, 1) 177 | Menu, % this.id, Add, % "Select Binding...", % fn 178 | 179 | fn := this.IOControlChoiceMade.Bind(this, 2) 180 | Menu, % this.id, Add, % "Block", % fn 181 | 182 | fn := this.IOControlChoiceMade.Bind(this, 3) 183 | Menu, % this.id, Add, % "Wild", % fn 184 | 185 | fn := this.IOControlChoiceMade.Bind(this, 4) 186 | Menu, % this.id, Add, % "Suppress Repeats", % fn 187 | 188 | fn := this.IOControlChoiceMade.Bind(this, 5) 189 | Menu, % this.id, Add, % "Clear", % fn 190 | 191 | } 192 | 193 | SetBinding(bo){ 194 | if (IsObject(bo)){ 195 | bo := bo 196 | this.BindObject := bo 197 | } else { 198 | this.BindObject.Binding := [] 199 | } 200 | this.parent.InputThread.UpdateBinding(this.id, this.BindObject) 201 | GuiControl, % this.parent.hwnd ":" , % this.hReadout, % this.BuildHumanReadable() 202 | for opt, state in bo.BindOptions { 203 | this.SetMenuCheckState(opt, state) 204 | } 205 | } 206 | 207 | IOControlChoiceMade(val){ 208 | if (val == 1){ 209 | ; Bind 210 | this.parent.InputThread.SetDetectionState(0) 211 | this.parent.StartBindMode(this.BindModeEnded.Bind(this)) 212 | } else if (val == 2){ 213 | ; Block 214 | this.BindObject.BindOptions.Block := !this.BindObject.BindOptions.Block 215 | this.SetMenuCheckState("Block") 216 | this.BindModeEnded(this.BindObject) 217 | } else if (val == 3){ 218 | ; Wild 219 | this.BindObject.BindOptions.Wild := !this.BindObject.BindOptions.Wild 220 | this.SetMenuCheckState("Wild") 221 | this.BindModeEnded(this.BindObject) 222 | } else if (val == 4){ 223 | ; Suppress Repeats 224 | this.BindObject.BindOptions.Suppress := !this.BindObject.BindOptions.Suppress 225 | this.SetMenuCheckState("Suppress") 226 | this.BindModeEnded(this.BindObject) 227 | } else if (val == 5){ 228 | ; Clear 229 | this.BindObject := new this.parent._BindObject() 230 | this.BindModeEnded(this.BindObject) 231 | } 232 | } 233 | 234 | SetMenuCheckState(which){ 235 | state := this.BindObject.BindOptions[which] 236 | try Menu, % this.id, % (state ? "Check" : "UnCheck"), % which 237 | } 238 | 239 | BindModeEnded(bo){ 240 | this.SetBinding(bo) 241 | this.parent._BindingChanged(this.id, bo) 242 | this.parent.InputThread.SetDetectionState(1) 243 | } 244 | 245 | OpenMenu(){ 246 | ControlGetPos, cX, cY, cW, cH,, % "ahk_id " this.hReadout 247 | Menu, % this.id, Show, % cX+1, % cY + cH 248 | } 249 | 250 | ; Builds a human-readable form of the BindObject 251 | BuildHumanReadable(){ 252 | str := "" 253 | if (!this.BindObject.IOClass){ 254 | str := "Select an Input Button..." 255 | } else if (this.BindObject.IOClass == "AHK_KBM_Input"){ 256 | max := this.BindObject.Binding.length() 257 | Loop % max { 258 | str .= this.BuildKeyName(this.BindObject.Binding[A_Index]) 259 | if (A_Index != max) 260 | str .= " + " 261 | } 262 | } else if (this.BindObject.IOClass == "AHK_JoyBtn_Input"){ 263 | return "Stick " this.BindObject.DeviceID " Button " this.BindObject.Binding[1] 264 | } else if (this.BindObject.IOClass == "AHK_JoyHat_Input"){ 265 | static hat_directions := ["Up", "Right", "Down", "Left"] 266 | return "Stick " this.BindObject.DeviceID ", Hat " hat_directions[this.BindObject.Binding[1]] 267 | } 268 | return str 269 | } 270 | 271 | ; Builds the AHK key name 272 | BuildKeyName(code){ 273 | static replacements := {33: "PgUp", 34: "PgDn", 35: "End", 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 45: "Insert", 46: "Delete"} 274 | static additions := {14: "NumpadEnter"} 275 | if (ObjHasKey(replacements, code)){ 276 | return replacements[code] 277 | } else if (ObjHasKey(additions, code)){ 278 | return additions[code] 279 | } else { 280 | return GetKeyName("vk" Format("{:x}", code)) 281 | } 282 | } 283 | 284 | ; Returns true if this Button is a modifier key on the keyboard 285 | IsModifier(code){ 286 | return ObjHasKey(this._Modifiers, code) 287 | } 288 | 289 | ; Renders the keycode of a Modifier to it's AHK Hotkey symbol (eg 162 for LCTRL to ^) 290 | RenderModifier(code){ 291 | return this._Modifiers[code].s 292 | } 293 | } 294 | 295 | ; ====================================== BINDMODE THREAD ============================================== 296 | ; An additional thread that is always running and handles detection of input while in Bind Mode (User selecting hotkeys) 297 | InitBindMode(){ 298 | 299 | this._BindModeThread := new _BindMapper(this.ProcessBindModeInput.Bind(this)) 300 | 301 | Gui, +HwndhOld 302 | Gui, new, +HwndHwnd 303 | Gui +ToolWindow -Border 304 | Gui, Font, S15 305 | Gui, Color, Red 306 | this.hBindModePrompt := hwnd 307 | Gui, Add, Text, Center, Press the button(s) you wish to bind to this control.`n`nBind Mode will end when you release a key. 308 | Gui, % hOld ":Default" 309 | } 310 | 311 | ;IOClassMappings, this._BindModeEnded.Bind(this, callback) 312 | StartBindMode(callback){ 313 | IOClassMappings := {AHK_Common: 0, AHK_KBM_Input: "AHK_KBM_Input", AHK_JoyBtn_Input: "AHK_JoyBtn_Input", AHK_JoyHat_Input: "AHK_JoyHat_Input"} 314 | this._callback := callback 315 | 316 | this.SelectedBinding := new this._BindObject() 317 | this.BindMode := 1 318 | this.EndKey := 0 319 | this.HeldModifiers := {} 320 | this.ModifierCount := 0 321 | ; IOClassMappings controls which type each IOClass reports as. 322 | ; ie we need the AHK_KBM_Input class to report as AHK_KBM_Output when we are binding an output key 323 | this.IOClassMappings := IOClassMappings 324 | this.SetHotkeyState(1) 325 | } 326 | 327 | ; Bind Mode ended. Pass the BindObject and it's IOClass back to the GuiControl that requested the binding 328 | _BindModeEnded(callback, bo){ 329 | OutputDebug % "UCR| UCR: Bind Mode Ended. Binding[1]: " bo.Binding[1] ", DeviceID: " bo.DeviceID ", IOClass: " this.SelectedBinding.IOClass 330 | callback.Call(bo) 331 | } 332 | 333 | ; Turns on or off the hotkeys 334 | SetHotkeyState(state){ 335 | global _AppFactoryBindMode 336 | _AppFactoryBindMode := state 337 | if (state){ 338 | Gui, % this.hBindModePrompt ":Show" 339 | ;UCR.MoveWindowToCenterOfGui(this.hBindModePrompt) 340 | } else { 341 | Gui, % this.hBindModePrompt ":Hide" 342 | } 343 | this._BindModeThread.SetDetectionState(state, this.IOClassMappings) 344 | } 345 | 346 | ; The BindModeThread calls back here 347 | ProcessBindModeInput(e, i, deviceid, IOClass){ 348 | ;ToolTip % "e " e ", i " i ", deviceid " deviceid ", IOClass " IOClass 349 | ;if (ObjHasKey(this._Modifiers, i)) 350 | if (this.SelectedBinding.IOClass && (this.SelectedBinding.IOClass != IOClass)){ 351 | ; Changed binding IOCLass part way through. 352 | if (e){ 353 | SoundBeep, 500, 100 354 | } 355 | return 356 | } 357 | max := this.SelectedBinding.Binding.length() 358 | if (e){ 359 | for idx, code in this.SelectedBinding.Binding { 360 | if (i == code) 361 | return ; filter repeats 362 | } 363 | this.SelectedBinding.Binding.push(i) 364 | this.SelectedBinding.DeviceID := DeviceID 365 | if (this.AHK_KBM_Input.IsModifier(i)){ 366 | if (max > this.ModifierCount){ 367 | ; Modifier pressed after end key 368 | SoundBeep, 500, 100 369 | return 370 | } 371 | this.ModifierCount++ 372 | } else if (max > this.ModifierCount) { 373 | ; Second End Key pressed after first held 374 | SoundBeep, 500, 100 375 | return 376 | } 377 | this.SelectedBinding.IOClass := IOClass 378 | } else { 379 | this.BindMode := 0 380 | this.SetHotkeyState(0, this.IOClassMappings) 381 | ;ret := {Binding:[i], DeviceID: deviceid, IOClass: this.IOClassMappings[IOClass]} 382 | 383 | ;OutputDebug % "UCR| BindModeHandler: Bind Mode Ended. Binding[1]: " this.SelectedBinding.Binding[1] ", DeviceID: " this.SelectedBinding.DeviceID ", IOClass: " this.SelectedBinding.IOClass 384 | this._Callback.Call(this.SelectedBinding) 385 | } 386 | } 387 | 388 | ; ====================================== INPUT THREAD ============================================== 389 | InitInputThread(){ 390 | this.InputThread := new _InputThread(this.InputEvent.Bind(this)) 391 | } 392 | 393 | InputEvent(ControlGUID, e){ 394 | ; Suppress repeats 395 | if (this.IOControls[ControlGuid].BindObject.BindOptions.Suppress && (this.IOControls[ControlGuid].State == e)) 396 | return 397 | this.IOControls[ControlGuid].State := e 398 | ; Fire the callback 399 | this.IOControls[ControlGuid].Callback.call(e) 400 | } 401 | 402 | ; ====================================== MISC ============================================== 403 | ; Describes a binding. Used internally and dumped to the INI file 404 | class _BindObject { 405 | IOClass := "" 406 | DeviceID := 0 ; Device ID, eg Stick ID for Joystick input or vGen output 407 | Binding := [] ; Codes of the input(s) for the Binding. Is an indexed array once set 408 | ; Normally a single element, but for KBM could be up to 4 modifiers plus a key/button 409 | BindOptions := {Block: 0, Wild: 0, Suppress: 0} 410 | } 411 | } -------------------------------------------------------------------------------- /AppFactory_H/Source/InputThread.ahk: -------------------------------------------------------------------------------- 1 | ; Can use #Include %A_LineFile%\..\other.ahk to include in same folder 2 | Class _InputThread { 3 | static IOClasses := {AHK_KBM_Input: 0, AHK_JoyBtn_Input: 0, AHK_JoyHat_Input: 0} 4 | DetectionState := 0 5 | UpdateBindingQueue := [] ; An array of bindings waiting to be updated. 6 | UpdatingBindings := 0 7 | ControlMappings := {} 8 | 9 | __New(ProfileID, CallbackPtr){ 10 | this.Callback := ObjShare(CallbackPtr) 11 | ;this.Callback := CallbackPtr 12 | this.ProfileID := ProfileID ; Profile ID of parent profile. So we know which profile this thread serves 13 | names := "" 14 | i := 0 15 | ; Instantiate each of the IOClasses specified in the IOClasses array 16 | for name, state in this.IOClasses { 17 | ; Instantiate an instance of a class that is a child class of this one. Thanks to HotkeyIt for this code! 18 | ; Replace each 0 in the array with an instance of the relevant class 19 | call:=this.base[name] 20 | this.IOClasses[name] := new call(this.Callback) 21 | ; debugging string 22 | if (i) 23 | names .= ", " 24 | names .= name 25 | i++ 26 | } 27 | if (i){ 28 | ; OutputDebug % "UCR| Input Thread loaded IOClasses: " names 29 | } else { 30 | OutputDebug % "UCR| Input Thread WARNING! Loaded No IOClasses!" 31 | } 32 | 33 | ; Set up interfaces that the main thread can call 34 | global InterfaceUpdateBinding := ObjShare(this.UpdateBinding.Bind(this)) 35 | ;global InterfaceUpdateBindings := ObjShare(this.UpdateBindings.Bind(this)) 36 | global InterfaceSetDetectionState := ObjShare(this.SetDetectionState.Bind(this)) 37 | 38 | ; Get a boundfunc for the method that processes binding updates 39 | ;this.BindingQueueFn := this._ProcessBindingQueue.Bind(this) 40 | 41 | ; Unreachable dummy label for hotkeys to bind to to clear binding 42 | if(0){ 43 | UCR_INPUTHREAD_DUMMY_LABEL: 44 | return 45 | } 46 | 47 | } 48 | 49 | UpdateBinding(ControlGUID, boPtr){ 50 | bo := ObjShare(boPtr).clone() 51 | iom := this.ControlMappings[ControlGuid] 52 | if (this.ControlMappings.HasKey(ControlGuid) && iom != bo.IOClass){ 53 | this.IOClasses[iom].RemoveBinding(ControlGUID) 54 | } 55 | this.ControlMappings[ControlGuid] := bo.IOClass 56 | ;OutputDebug % "UCR| Updating binding for ControlGUID " ControlGUID ", IOClass " bo.IOClass 57 | ; Direct the request to the appropriate IOClass that handles it 58 | this.IOClasses[bo.IOClass].UpdateBinding(ControlGUID, bo) 59 | } 60 | 61 | ;~ _SetDetectionState(state){ 62 | SetDetectionState(state){ 63 | OutputDebug % "UCR| InputThread: Hotkey detection " (state ? "On" : "Off") 64 | if (state == this.DetectionState) 65 | return 66 | this.DetectionState := state 67 | for name, cls in this.IOClasses { 68 | cls.SetDetectionState(state) 69 | } 70 | } 71 | 72 | ; Listens for Keyboard and Mouse input using the AHK Hotkey command 73 | class AHK_KBM_Input { 74 | DetectionState := 0 75 | _AHKBindings := {} 76 | 77 | __New(callback){ 78 | this.callback := callback 79 | Suspend, On ; Start with detection off, even if we are passed bindings 80 | } 81 | 82 | UpdateBinding(ControlGUID, bo){ 83 | this.RemoveBinding(ControlGUID) 84 | if (bo.Binding[1]){ 85 | keyname := "$" this.BuildHotkeyString(bo) 86 | fn := this.KeyEvent.Bind(this, ControlGUID, 1) 87 | hotkey, % keyname, % fn, On 88 | fn := this.KeyEvent.Bind(this, ControlGUID, 0) 89 | hotkey, % keyname " up", % fn, On 90 | ;OutputDebug % "UCR| AHK_KBM_Input Added hotkey " keyname " for ControlGUID " ControlGUID 91 | this._AHKBindings[ControlGUID] := keyname 92 | } 93 | } 94 | 95 | SetDetectionState(state){ 96 | ; Are we already in the requested state? 97 | ; This code is rigged so that either AHK_KBM_Input or AHK_JoyBtn_Input or both will not clash... 98 | ; ... As long as all are turned on or off together, you won't get weird results. 99 | if (A_IsSuspended == state){ 100 | ;OutputDebug % "UCR| Thread: AHK_KBM_Input IOClass turning Hotkey detection " (state ? "On" : "Off") 101 | Suspend, % (state ? "Off" : "On") 102 | } 103 | this.DetectionState := state 104 | } 105 | 106 | RemoveBinding(ControlGUID){ 107 | keyname := this._AHKBindings[ControlGUID] 108 | if (keyname){ 109 | ;OutputDebug % "UCR| AHK_KBM_Input Removing hotkey " keyname " for ControlGUID " ControlGUID 110 | hotkey, % keyname, UCR_INPUTHREAD_DUMMY_LABEL 111 | hotkey, % keyname, Off 112 | hotkey, % keyname " up", UCR_INPUTHREAD_DUMMY_LABEL 113 | hotkey, % keyname " up", Off 114 | this._AHKBindings.Delete(ControlGUID) 115 | } 116 | } 117 | 118 | KeyEvent(ControlGUID, e){ 119 | ;OutputDebug % "UCR| AHK_KBM_Input Key event for GuiControl " ControlGUID 120 | fn := this.InputEvent.Bind(this, ControlGUID, e) 121 | SetTimer, % fn, -0 122 | } 123 | 124 | InputEvent(ControlGUID, state){ 125 | this.Callback.Call(ControlGUID, state) 126 | } 127 | 128 | ; Builds an AHK hotkey string (eg ~^a) from a BindObject 129 | BuildHotkeyString(bo){ 130 | if (!bo.Binding.Length()) 131 | return "" 132 | str := "" 133 | if (bo.BindOptions.Wild) 134 | str .= "*" 135 | if (!bo.BindOptions.Block) 136 | str .= "~" 137 | max := bo.Binding.Length() 138 | Loop % max { 139 | key := bo.Binding[A_Index] 140 | if (A_Index = max){ 141 | islast := 1 142 | nextkey := 0 143 | } else { 144 | islast := 0 145 | nextkey := bo[A_Index+1] 146 | } 147 | if (this.IsModifier(key) && (max > A_Index)){ 148 | str .= this.RenderModifier(key) 149 | } else { 150 | str .= this.BuildKeyName(key) 151 | } 152 | } 153 | return str 154 | } 155 | 156 | ; === COMMON WITH IOCLASS. MOVE TO INCLUDE ===== 157 | static _Modifiers := ({91: {s: "#", v: "<"},92: {s: "#", v: ">"} 158 | ,160: {s: "+", v: "<"},161: {s: "+", v: ">"} 159 | ,162: {s: "^", v: "<"},163: {s: "^", v: ">"} 160 | ,164: {s: "!", v: "<"},165: {s: "!", v: ">"}}) 161 | 162 | ; Builds the AHK key name 163 | BuildKeyName(code){ 164 | static replacements := {33: "PgUp", 34: "PgDn", 35: "End", 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 45: "Insert", 46: "Delete"} 165 | static additions := {14: "NumpadEnter"} 166 | if (ObjHasKey(replacements, code)){ 167 | return replacements[code] 168 | } else if (ObjHasKey(additions, code)){ 169 | return additions[code] 170 | } else { 171 | return GetKeyName("vk" Format("{:x}", code)) 172 | } 173 | } 174 | 175 | ; Returns true if this Button is a modifier key on the keyboard 176 | IsModifier(code){ 177 | return ObjHasKey(this._Modifiers, code) 178 | } 179 | 180 | ; Renders the keycode of a Modifier to it's AHK Hotkey symbol (eg 162 for LCTRL to ^) 181 | RenderModifier(code){ 182 | return this._Modifiers[code].s 183 | } 184 | ; ================= END MOVE TO INCLUDE ====================== 185 | } 186 | 187 | ; Listens for Joystick Button input using AHK's Hotkey command 188 | ; Joystick button Hotkeys in AHK immediately fire the up event after the down event... 189 | ; ... so up events are emulated up using AHK's GetKeyState() function 190 | class AHK_JoyBtn_Input { 191 | HeldButtons := {} 192 | TimerWanted := 0 ; Whether or not we WANT to run the ButtonTimer (NOT if it is actually running!) 193 | TimerRunning := 0 194 | DetectionState := 0 ; Whether or not we are allowed to have hotkeys or be running the timer 195 | 196 | __New(Callback){ 197 | this.Callback := Callback 198 | this.TimerFn := this.ButtonWatcher.Bind(this) 199 | Suspend, On ; Start with detection off, even if we are passed bindings 200 | } 201 | 202 | UpdateBinding(ControlGUID, bo){ 203 | this.RemoveBinding(ControlGUID) 204 | if (bo.Binding[1]){ 205 | keyname := this.BuildHotkeyString(bo) 206 | fn := this.KeyEvent.Bind(this, ControlGUID, 1) 207 | if (GetKeyState(bo.DeviceID "JoyAxes")) 208 | try { 209 | hotkey, % keyname, % fn, On 210 | } 211 | else 212 | OutputDebug % "UCR| Warning! AHK_JoyBtn_Input did not declare hotkey " keyname " because the stick is disconnected" 213 | ;OutputDebug % "UCR| AHK_JoyBtn_Input Added hotkey " keyname " for ControlGUID " ControlGUID 214 | this._AHKBindings[ControlGUID] := keyname 215 | } 216 | } 217 | 218 | SetDetectionState(state){ 219 | ; Are we already in the requested state? 220 | if (A_IsSuspended == state){ 221 | ;OutputDebug % "UCR| Thread: AHK_JoyBtn_Input IOClass turning Hotkey detection " (state ? "On" : "Off") 222 | Suspend, % (state ? "Off" : "On") 223 | } 224 | this.DetectionState := state 225 | this.ProcessTimerState() 226 | } 227 | 228 | RemoveBinding(ControlGUID){ 229 | keyname := this._AHKBindings[ControlGUID] 230 | if (keyname){ 231 | ;OutputDebug % "UCR| AHK_JoyBtn_Input Removing hotkey " keyname " for ControlGUID " ControlGUID 232 | try{ 233 | hotkey, % keyname, UCR_INPUTHREAD_DUMMY_LABEL 234 | } 235 | try{ 236 | hotkey, % keyname, Off 237 | } 238 | this._AHKBindings.Delete(ControlGUID) 239 | } 240 | ;this._CurrentBinding := 0 241 | } 242 | 243 | KeyEvent(ControlGUID, e){ 244 | ; ToDo: Parent will not exist in thread! 245 | 246 | ;OutputDebug % "UCR| AHK_JoyBtn_Input Key event " e " for GuiControl " ControlGUID 247 | ;this.Callback.Call(ControlGUID, e) 248 | fn := this.InputEvent.Bind(this, ControlGUID, e) 249 | SetTimer, % fn, -0 250 | 251 | this.HeldButtons[this._AHKBindings[ControlGUID]] := ControlGUID 252 | if (!this.TimerWanted){ 253 | this.TimerWanted := 1 254 | this.ProcessTimerState() 255 | } 256 | } 257 | 258 | InputEvent(ControlGUID, state){ 259 | this.Callback.Call(ControlGUID, state) 260 | } 261 | 262 | ButtonWatcher(){ 263 | for bindstring, ControlGUID in this.HeldButtons { 264 | if (!GetKeyState(bindstring)){ 265 | this.HeldButtons.Delete(bindstring) 266 | ;OutputDebug % "UCR| AHK_JoyBtn_Input Key event 0 for GuiControl " ControlGUID 267 | ;this.Callback.Call(ControlGUID, 0) 268 | fn := this.InputEvent.Bind(this, ControlGUID, 0) 269 | SetTimer, % fn, -0 270 | if (IsEmptyAssoc(this.HeldButtons)){ 271 | this.TimerWanted := 0 272 | this.ProcessTimerState() 273 | return 274 | } 275 | } 276 | } 277 | } 278 | 279 | ProcessTimerState(){ 280 | fn := this.TimerFn 281 | if (this.TimerWanted && this.DetectionState && !this.TimerRunning){ 282 | SetTimer, % fn, 10 283 | this.TimerRunning := 1 284 | ;OutputDebug % "UCR| AHK_JoyBtn_Input Started ButtonWatcher " ControlGUID 285 | } else if ((!this.TimerWanted || !this.DetectionState) && this.TimerRunning){ 286 | SetTimer, % fn, Off 287 | this.TimerRunning := 0 288 | ;OutputDebug % "UCR| AHK_JoyBtn_Input Stopped ButtonWatcher " ControlGUID 289 | } 290 | } 291 | 292 | BuildHotkeyString(bo){ 293 | return bo.Deviceid "Joy" bo.Binding[1] 294 | } 295 | } 296 | 297 | ; Listens for Joystick Hat input using AHK's GetKeyState() function 298 | class AHK_JoyHat_Input { 299 | ; Indexed by GetKeyState string (eg "1JoyPOV") 300 | ; The HatWatcher timer is active while this array has items. 301 | ; Contains an array of objects whose keys are the GUIDs of GuiControls mapped to that POV 302 | ; Properties of those keys are the direction of the mapping and the state of the binding 303 | HatBindings := {} 304 | 305 | ; GUID-Indexed array of sticks + directions that each GUIControl is mapped to, plus it's current state 306 | ControlMappings := {} 307 | 308 | ; Which cardinal directions are pressed for each of the 8 compass directions, plus centre 309 | ; Order is U, R, D, L 310 | static PovMap := {-1: [0,0,0,0], 1: [1,0,0,0], 2: [1,1,0,0] , 3: [0,1,0,0], 4: [0,1,1,0], 5: [0,0,1,0], 6: [0,0,1,1], 7: [0,0,0,1], 8: [1,0,0,1]} 311 | 312 | TimerRunning := 0 313 | TimerWanted := 0 314 | ConnectedSticks := [0,0,0,0,0,0,0,0] 315 | 316 | __New(Callback){ 317 | this.Callback := Callback 318 | 319 | this.TimerFn := this.HatWatcher.Bind(this) 320 | } 321 | 322 | ; Request from main thread to update binding 323 | UpdateBinding(ControlGUID, bo){ 324 | ;OutputDebug % "UCR| AHK_JoyHat_Input " (bo.Binding[1] ? "Update" : "Remove" ) " Hat Binding - Device: " bo.DeviceID ", Direction: " bo.Binding[1] 325 | this._UpdateArrays(ControlGUID, bo) 326 | this.TimerWanted := !IsEmptyAssoc(this.ControlMappings) 327 | this.ProcessTimerState() 328 | } 329 | 330 | SetDetectionState(state){ 331 | this.DetectionState := state 332 | this.ProcessTimerState() 333 | } 334 | 335 | ProcessTimerState(){ 336 | fn := this.TimerFn 337 | if (this.TimerWanted && this.DetectionState && !this.TimerRunning){ 338 | ; Pre-cache connected sticks, as polling disconnected sticks takes lots of CPU 339 | Loop 8 { 340 | this.ConnectedSticks[A_Index] := GetKeyState(A_Index "JoyInfo") 341 | } 342 | SetTimer, % fn, 10 343 | this.TimerRunning := 1 344 | ;OutputDebug % "UCR| AHK_JoyHat_Input Started HatWatcher" 345 | } else if ((!this.TimerWanted || !this.DetectionState) && this.TimerRunning){ 346 | SetTimer, % fn, Off 347 | this.TimerRunning := 0 348 | ;OutputDebug % "UCR| AHK_JoyHat_Input Stopped HatWatcher" 349 | } 350 | } 351 | 352 | ; Updates the arrays which drive hat detection 353 | _UpdateArrays(ControlGUID, bo := 0){ 354 | if (ObjHasKey(this.ControlMappings, ControlGUID)){ 355 | ; GuiControl already has binding 356 | bindstring := this.ControlMappings[ControlGUID].bindstring 357 | this.HatBindings[bindstring].Delete(ControlGUID) 358 | this.ControlMappings.Delete(ControlGUID) 359 | if (IsEmptyAssoc(this.HatBindings[bindstring])){ 360 | this.HatBindings.Delete(bindstring) 361 | ;OutputDebug % "UCR| AHK_JoyHat_Input Removing Hat Bindstring " bindstring 362 | } 363 | } 364 | if (bo != 0 && bo.Binding[1]){ 365 | ; there is a new binding 366 | bindstring := bo.DeviceID "JoyPOV" 367 | if (!ObjHasKey(this.HatBindings, bindstring)){ 368 | this.HatBindings[bindstring] := {} 369 | ;OutputDebug % "UCR| AHK_JoyHat_Input Adding Hat Bindstring " bindstring 370 | } 371 | this.HatBindings[bindstring, ControlGUID] := {dir: bo.Binding[1], state: 0} 372 | this.ControlMappings[ControlGUID] := {bindstring: bindstring} 373 | } 374 | } 375 | 376 | ; Called on a timer when we are trying to detect hats 377 | HatWatcher(){ 378 | for bindstring, bindings in this.HatBindings { 379 | if (!this.ConnectedSticks[SubStr(bindstring, 1, 1)]){ 380 | ; Do not poll unconnected sticks, it consumes a lot of cpu 381 | continue 382 | } 383 | state := GetKeyState(bindstring) 384 | state := (state = -1 ? -1 : round(state / 4500) + 1) 385 | for ControlGUID, obj in bindings { 386 | new_state := (this.PovMap[state, obj.dir] == 1) 387 | if (obj.state != new_state){ 388 | obj.state := new_state 389 | ;OutputDebug % "UCR| InputThread: AHK_JoyHat_Input Direction " obj.dir " state " new_state " calling ControlGUID " ControlGUID 390 | ; Use the thread-safe object to tell the main thread that the hat direction changed state 391 | ;this.Callback.Call(ControlGUID, new_state) 392 | fn := this.InputEvent.Bind(this, ControlGUID, new_state) 393 | SetTimer, % fn, -0 394 | } 395 | } 396 | } 397 | } 398 | 399 | InputEvent(ControlGUID, state){ 400 | this.Callback.Call(ControlGUID, state) 401 | } 402 | } 403 | } 404 | 405 | ; Is an associative array empty? 406 | IsEmptyAssoc(assoc){ 407 | return !assoc._NewEnum()[k, v] 408 | } -------------------------------------------------------------------------------- /AppFactory_H/Source/AppFactory.ahk: -------------------------------------------------------------------------------- 1 | #include %A_LineFile%\..\JSON.ahk 2 | 3 | Class AppFactory { 4 | _ThreadHeader := "`n#Persistent`n#NoTrayIcon`n#MaxHotkeysPerInterval 9999`n" 5 | _ThreadFooter := "`nautoexecute_done := 1`nreturn`n" 6 | InputThread := 0 7 | IOControls := {} 8 | GuiControls := {} 9 | Settings := {} 10 | 11 | ; ====================== PUBLIC METHODS. USER SCRIPTS SHOULD ONLY CALL THESE ======================== 12 | AddInputButton(guid, options, callback){ 13 | this.IOControls[guid] := new this._IOControl(this, guid, options, callback) 14 | this.IOControls[guid].SetBinding(this.Settings.IOControls[guid]) 15 | } 16 | 17 | AddControl(guid, ctrltype, options := "", default := "", callback := 0){ 18 | this.GuiControls[guid] := new this._GuiControl(this, guid, ctrltype, options, default, callback) 19 | if (this.Settings.GuiControls.Haskey(guid)){ 20 | this.GuiControls[guid].SetValue(this.Settings.GuiControls[guid]) 21 | } else { 22 | if (this.GuiControls[guid].IsListType){ 23 | d := RegExMatch(default, "(.*)\|\|", out) 24 | default := out1 25 | } 26 | this.GuiControls[guid].SetValue(default) 27 | } 28 | 29 | } 30 | 31 | ; ====================== PRIVATE METHODS. USER SCRIPTS SHOULD NOT CALL THESE ======================== 32 | __New(hwnd := 0){ 33 | this._SettingsFile := RegExReplace(A_ScriptName, ".ahk|.exe", ".ini") 34 | 35 | this.InitBindMode() 36 | this.InitInputThread() 37 | 38 | if (hwnd == 0) 39 | Gui, +Hwndhwnd 40 | this.hwnd := hwnd 41 | 42 | FileRead, j, % this._SettingsFile 43 | if (j == ""){ 44 | j := {IOControls: {}, GuiControls: {}} 45 | } else { 46 | j := JSON.Load(j) 47 | } 48 | this.Settings := j 49 | this.InputThread.SetDetectionState(1) 50 | } 51 | 52 | ; When bind mode ends, the GuiControl will call this method to request that the setting be saved 53 | _BindingChanged(ControlGuid, bo){ 54 | this.Settings.IOControls[ControlGuid] := bo 55 | this._SaveSettings() 56 | } 57 | 58 | _GuiControlChanged(ControlGuid, value){ 59 | this.Settings.GuiControls[ControlGuid] := value 60 | this._SaveSettings() 61 | } 62 | 63 | _SaveSettings(){ 64 | FileReplace(JSON.Dump(this.Settings, ,true), this._SettingsFile) 65 | } 66 | 67 | ; ============================================================================================ 68 | ; ==================================== GUICONTROLS =========================================== 69 | ; ============================================================================================ 70 | class _GuiControl { 71 | static _ListTypes := {ListBox: 1, DDL: 1, DropDownList: 1, ComboBox: 1, Tab: 1, Tab2: 1, Tab3: 1} 72 | _Value := "" 73 | 74 | Get(){ 75 | return this._Value 76 | } 77 | 78 | __New(parent, guid, ctrltype, options, default, callback){ 79 | this.id := guid 80 | this.parent := parent 81 | this.Callback := callback 82 | this.Default := default 83 | 84 | if (ObjHasKey(this._ListTypes, ctrltype)){ 85 | this.IsListType := 1 86 | ; Detect if this List Type uses AltSubmit 87 | if (InStr(options, "altsubmit")) 88 | this.IsAltSubmitType := 1 89 | else 90 | this.IsAltSubmitType := 0 91 | } else { 92 | this.IsListType := 0 93 | this.IsAltSubmitType := 0 94 | } 95 | 96 | Gui, % this.parent.hwnd ":Add", % ctrltype, % "hwndhwnd " options, % default 97 | this.hwnd := hwnd 98 | fn := this.ControlChanged.Bind(this) 99 | this.ChangeValueFn := fn 100 | this._SetGLabel(1) 101 | 102 | return this 103 | } 104 | 105 | SetControlState(value){ 106 | this._SetGlabel(0) ; Turn off g-label to avoid triggering save 107 | cmd := "" 108 | if (this.IsListType){ 109 | cmd := (this.IsAltSubmitType ? "choose" : "choosestring") 110 | } 111 | GuiControl, % this.parent.hwnd ":" cmd, % this.hwnd, % value 112 | this._SetGlabel(1) ; Turn g-label back on 113 | } 114 | 115 | ; Turns on or off the g-label for the GuiControl 116 | ; This is needed to work around not being able to programmatically set GuiControl without triggering g-label 117 | _SetGlabel(state){ 118 | if (state){ 119 | fn := this.ChangeValueFn 120 | GuiControl, % this.parent.hwnd ":+g", % this.hwnd, % fn 121 | } else { 122 | GuiControl, % this.parent.hwnd ":-g", % this.hwnd 123 | } 124 | } 125 | 126 | ; User interacted with GuiControl 127 | ControlChanged(){ 128 | GuiControlGet, value, % this.parent.hwnd ":" , % this.hwnd 129 | this._Value := value 130 | if (this.Callback != 0){ 131 | this.Callback.call(value) 132 | } 133 | this.parent._GuiControlChanged(this.id, value) 134 | } 135 | 136 | ; Called on load of settings 137 | SetValue(value){ 138 | this._Value := value 139 | this.Callback.call(value) 140 | this.SetControlState(value) 141 | } 142 | } 143 | 144 | ; ============================================================================================ 145 | ; ==================================== IOCONTROLS ============================================ 146 | ; ============================================================================================ 147 | class _IOControl { 148 | guid := 0 ; The unique ID/Name for this IOControl 149 | Callback := 0 ; Holds the user's callback for this IOControl 150 | BindObject := 0 ; Holds the BindObject describing the current binding 151 | State := 0 ; The State of the input. Only really used for Repeat Suppression 152 | 153 | static _Modifiers := ({91: {s: "#", v: "<"},92: {s: "#", v: ">"} 154 | ,160: {s: "+", v: "<"},161: {s: "+", v: ">"} 155 | ,162: {s: "^", v: "<"},163: {s: "^", v: ">"} 156 | ,164: {s: "!", v: "<"},165: {s: "!", v: ">"}}) 157 | 158 | __New(parent, guid, options, callback){ 159 | this.id := guid 160 | this.parent := parent 161 | this.Callback := callback 162 | this.BindObject := new this.parent._BindObject() 163 | Gui, % this.parent.hwnd ":Add", Button, % "hwndhReadout " options , Select an Input Button 164 | this.hReadout := hReadout 165 | fn := this.OpenMenu.Bind(this) 166 | GuiControl, % this.parent.hwnd ":+g", % hReadout, % fn 167 | 168 | fn := this.IOControlChoiceMade.Bind(this, 1) 169 | Menu, % this.id, Add, % "Select Binding...", % fn 170 | 171 | fn := this.IOControlChoiceMade.Bind(this, 2) 172 | Menu, % this.id, Add, % "Block", % fn 173 | 174 | fn := this.IOControlChoiceMade.Bind(this, 3) 175 | Menu, % this.id, Add, % "Wild", % fn 176 | 177 | fn := this.IOControlChoiceMade.Bind(this, 4) 178 | Menu, % this.id, Add, % "Suppress Repeats", % fn 179 | 180 | fn := this.IOControlChoiceMade.Bind(this, 5) 181 | Menu, % this.id, Add, % "Clear", % fn 182 | 183 | } 184 | 185 | SetBinding(bo){ 186 | if (IsObject(bo)){ 187 | bo := bo 188 | this.BindObject := bo 189 | } else { 190 | this.BindObject.Binding := [] 191 | } 192 | this.parent.InputThread.UpdateBinding(this.id, ObjShare(this.BindObject)) 193 | GuiControl, % this.parent.hwnd ":" , % this.hReadout, % this.BuildHumanReadable() 194 | for opt, state in bo.BindOptions { 195 | this.SetMenuCheckState(opt, state) 196 | } 197 | } 198 | 199 | IOControlChoiceMade(val){ 200 | if (val == 1){ 201 | ; Bind 202 | this.parent.InputThread.SetDetectionState(0) 203 | this.parent.StartBindMode(this.BindModeEnded.Bind(this)) 204 | } else if (val == 2){ 205 | ; Block 206 | this.BindObject.BindOptions.Block := !this.BindObject.BindOptions.Block 207 | this.SetMenuCheckState("Block") 208 | this.BindModeEnded(this.BindObject) 209 | } else if (val == 3){ 210 | ; Wild 211 | this.BindObject.BindOptions.Wild := !this.BindObject.BindOptions.Wild 212 | this.SetMenuCheckState("Wild") 213 | this.BindModeEnded(this.BindObject) 214 | } else if (val == 4){ 215 | ; Suppress Repeats 216 | this.BindObject.BindOptions.Suppress := !this.BindObject.BindOptions.Suppress 217 | this.SetMenuCheckState("Suppress") 218 | this.BindModeEnded(this.BindObject) 219 | } else if (val == 5){ 220 | ; Clear 221 | this.BindObject := new this.parent._BindObject() 222 | this.BindModeEnded(this.BindObject) 223 | } 224 | } 225 | 226 | SetMenuCheckState(which){ 227 | state := this.BindObject.BindOptions[which] 228 | try Menu, % this.id, % (state ? "Check" : "UnCheck"), % which 229 | } 230 | 231 | BindModeEnded(bo){ 232 | this.SetBinding(bo) 233 | this.parent._BindingChanged(this.id, bo) 234 | this.parent.InputThread.SetDetectionState(1) 235 | } 236 | 237 | OpenMenu(){ 238 | ControlGetPos, cX, cY, cW, cH,, % "ahk_id " this.hReadout 239 | Menu, % this.id, Show, % cX+1, % cY + cH 240 | } 241 | 242 | ; Builds a human-readable form of the BindObject 243 | BuildHumanReadable(){ 244 | str := "" 245 | if (!this.BindObject.IOClass){ 246 | str := "Select an Input Button..." 247 | } else if (this.BindObject.IOClass == "AHK_KBM_Input"){ 248 | max := this.BindObject.Binding.length() 249 | Loop % max { 250 | str .= this.BuildKeyName(this.BindObject.Binding[A_Index]) 251 | if (A_Index != max) 252 | str .= " + " 253 | } 254 | } else if (this.BindObject.IOClass == "AHK_JoyBtn_Input"){ 255 | return "Stick " this.BindObject.DeviceID " Button " this.BindObject.Binding[1] 256 | } else if (this.BindObject.IOClass == "AHK_JoyHat_Input"){ 257 | static hat_directions := ["Up", "Right", "Down", "Left"] 258 | return "Stick " this.BindObject.DeviceID ", Hat " hat_directions[this.BindObject.Binding[1]] 259 | } 260 | return str 261 | } 262 | 263 | ; Builds the AHK key name 264 | BuildKeyName(code){ 265 | static replacements := {33: "PgUp", 34: "PgDn", 35: "End", 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 45: "Insert", 46: "Delete"} 266 | static additions := {14: "NumpadEnter"} 267 | if (ObjHasKey(replacements, code)){ 268 | return replacements[code] 269 | } else if (ObjHasKey(additions, code)){ 270 | return additions[code] 271 | } else { 272 | return GetKeyName("vk" Format("{:x}", code)) 273 | } 274 | } 275 | 276 | ; Returns true if this Button is a modifier key on the keyboard 277 | IsModifier(code){ 278 | return ObjHasKey(this._Modifiers, code) 279 | } 280 | 281 | ; Renders the keycode of a Modifier to it's AHK Hotkey symbol (eg 162 for LCTRL to ^) 282 | RenderModifier(code){ 283 | return this._Modifiers[code].s 284 | } 285 | } 286 | 287 | ; ====================================== BINDMODE THREAD ============================================== 288 | ; An additional thread that is always running and handles detection of input while in Bind Mode (User selecting hotkeys) 289 | InitBindMode(){ 290 | ;~ FileRead, Script, % A_ScriptDir "\BindModeThread.ahk" 291 | FileRead, Script, % A_LineFile "\..\BindModeThread.ahk" 292 | this.__BindModeThread := AhkThread(this._ThreadHeader "`nBindMapper := new _BindMapper(" ObjShare(this.ProcessBindModeInput.Bind(this)) ")`n" this._ThreadFooter Script) 293 | While !this.__BindModeThread.ahkgetvar.autoexecute_done 294 | Sleep 50 ; wait until variable has been set. 295 | 296 | ; Create object to hold thread-safe boundfunc calls to the thread 297 | this._BindModeThread := {} 298 | this._BindModeThread.SetDetectionState := ObjShare(this.__BindModeThread.ahkgetvar("InterfaceSetDetectionState")) 299 | 300 | Gui, +HwndhOld 301 | Gui, new, +HwndHwnd 302 | Gui +ToolWindow -Border 303 | Gui, Font, S15 304 | Gui, Color, Red 305 | this.hBindModePrompt := hwnd 306 | Gui, Add, Text, Center, Press the button(s) you wish to bind to this control.`n`nBind Mode will end when you release a key. 307 | Gui, % hOld ":Default" 308 | } 309 | 310 | ;IOClassMappings, this._BindModeEnded.Bind(this, callback) 311 | StartBindMode(callback){ 312 | IOClassMappings := {AHK_Common: 0, AHK_KBM_Input: "AHK_KBM_Input", AHK_JoyBtn_Input: "AHK_JoyBtn_Input", AHK_JoyHat_Input: "AHK_JoyHat_Input"} 313 | this._callback := callback 314 | 315 | this.SelectedBinding := new this._BindObject() 316 | this.BindMode := 1 317 | this.EndKey := 0 318 | this.HeldModifiers := {} 319 | this.ModifierCount := 0 320 | ; IOClassMappings controls which type each IOClass reports as. 321 | ; ie we need the AHK_KBM_Input class to report as AHK_KBM_Output when we are binding an output key 322 | this.IOClassMappings := IOClassMappings 323 | this.SetHotkeyState(1) 324 | } 325 | 326 | ; Bind Mode ended. Pass the BindObject and it's IOClass back to the GuiControl that requested the binding 327 | _BindModeEnded(callback, bo){ 328 | ;OutputDebug % "UCR| UCR: Bind Mode Ended. Binding[1]: " bo.Binding[1] ", DeviceID: " bo.DeviceID ", IOClass: " this.SelectedBinding.IOClass 329 | callback.Call(bo) 330 | } 331 | 332 | ; Turns on or off the hotkeys 333 | SetHotkeyState(state){ 334 | if (state){ 335 | Gui, % this.hBindModePrompt ":Show" 336 | ;UCR.MoveWindowToCenterOfGui(this.hBindModePrompt) 337 | } else { 338 | Gui, % this.hBindModePrompt ":Hide" 339 | } 340 | ; Convert associative array to indexed, as ObjShare breaks associative array enumeration 341 | IOClassMappings := this.AssocToIndexed(this.IOClassMappings) 342 | this._BindModeThread.SetDetectionState(state, ObjShare(IOClassMappings)) 343 | } 344 | 345 | ; Converts an associative array to an indexed array of objects 346 | ; If you pass an associative array via ObjShare, you cannot enumerate it 347 | ; So each base key/value pair is added to an indexed array 348 | ; And the thread can re-build the associative array on the other end. 349 | AssocToIndexed(arr){ 350 | ret := [] 351 | for k, v in arr { 352 | ret.push({k: k, v: v}) 353 | } 354 | return ret 355 | } 356 | 357 | ; The BindModeThread calls back here 358 | ProcessBindModeInput(e, i, deviceid, IOClass){ 359 | ;ToolTip % "e " e ", i " i ", deviceid " deviceid ", IOClass " IOClass 360 | ;if (ObjHasKey(this._Modifiers, i)) 361 | if (this.SelectedBinding.IOClass && (this.SelectedBinding.IOClass != IOClass)){ 362 | ; Changed binding IOCLass part way through. 363 | if (e){ 364 | SoundBeep, 500, 100 365 | } 366 | return 367 | } 368 | max := this.SelectedBinding.Binding.length() 369 | if (e){ 370 | for idx, code in this.SelectedBinding.Binding { 371 | if (i == code) 372 | return ; filter repeats 373 | } 374 | this.SelectedBinding.Binding.push(i) 375 | this.SelectedBinding.DeviceID := DeviceID 376 | if (this.AHK_KBM_Input.IsModifier(i)){ 377 | if (max > this.ModifierCount){ 378 | ; Modifier pressed after end key 379 | SoundBeep, 500, 100 380 | return 381 | } 382 | this.ModifierCount++ 383 | } else if (max > this.ModifierCount) { 384 | ; Second End Key pressed after first held 385 | SoundBeep, 500, 100 386 | return 387 | } 388 | this.SelectedBinding.IOClass := IOClass 389 | } else { 390 | this.BindMode := 0 391 | this.SetHotkeyState(0, this.IOClassMappings) 392 | ;ret := {Binding:[i], DeviceID: deviceid, IOClass: this.IOClassMappings[IOClass]} 393 | 394 | ;OutputDebug % "UCR| BindModeHandler: Bind Mode Ended. Binding[1]: " this.SelectedBinding.Binding[1] ", DeviceID: " this.SelectedBinding.DeviceID ", IOClass: " this.SelectedBinding.IOClass 395 | this._Callback.Call(this.SelectedBinding) 396 | } 397 | } 398 | 399 | ; ====================================== INPUT THREAD ============================================== 400 | ; An additional thread that is always running and handles detection of input while in Normal Mode 401 | ; This is done in an additional thread so that fixes to joystick input (Buttons and Hats) do not have to have loops in the main thread 402 | InitInputThread(){ 403 | ;~ FileRead, Script, % A_ScriptDir "\InputThread.ahk" 404 | FileRead, Script, % A_LineFile "\..\InputThread.ahk" 405 | 406 | ; Cache script for profile InputThreads 407 | this._InputThreadScript := this._ThreadFooter Script 408 | this._StartInputThread() 409 | } 410 | 411 | ; Starts the "Input Thread" which handles detection of input 412 | _StartInputThread(){ 413 | if (this.InputThread == 0){ 414 | this.id := 1 415 | this._InputThread := AhkThread(this._ThreadHeader "`nInputThread := new _InputThread(""" this.id """," ObjShare(this.InputEvent.Bind(this)) ")`n" this._InputThreadScript) 416 | 417 | While !this._InputThread.ahkgetvar.autoexecute_done 418 | Sleep 10 ; wait until variable has been set. 419 | OutputDebug % "UCR| Input Thread started" 420 | 421 | ; Get thread-safe boundfunc object for thread's SetHotkeyState 422 | this.InputThread := {} 423 | this.InputThread.UpdateBinding := ObjShare(this._InputThread.ahkgetvar("InterfaceUpdateBinding")) 424 | ;this.InputThread.UpdateBindings := ObjShare(this._InputThread.ahkgetvar("InterfaceUpdateBindings")) 425 | this.InputThread.SetDetectionState := ObjShare(this._InputThread.ahkgetvar("InterfaceSetDetectionState")) 426 | } 427 | } 428 | 429 | InputEvent(ControlGUID, e){ 430 | ; Suppress repeats 431 | if (this.IOControls[ControlGuid].BindObject.BindOptions.Suppress && (this.IOControls[ControlGuid].State == e)) 432 | return 433 | this.IOControls[ControlGuid].State := e 434 | ; Fire the callback 435 | this.IOControls[ControlGuid].Callback.call(e) 436 | } 437 | 438 | ; ====================================== MISC ============================================== 439 | ; Describes a binding. Used internally and dumped to the INI file 440 | class _BindObject { 441 | IOClass := "" 442 | DeviceID := 0 ; Device ID, eg Stick ID for Joystick input or vGen output 443 | Binding := [] ; Codes of the input(s) for the Binding. Is an indexed array once set 444 | ; Normally a single element, but for KBM could be up to 4 modifiers plus a key/button 445 | BindOptions := {Block: 0, Wild: 0, Suppress: 0} 446 | } 447 | } --------------------------------------------------------------------------------