Now that with AHI we can make a 2nd keyboard totally independent of the 1st keyboard, my mind got to thinking of how you could take advantage of that.
I got to thinking about hotstrings - normally hotstrings have certain limitations because you are trying to preserve the normal functionality of the key, whilst also allowing hotstrings. But with AHI, we no longer care about the normal functionality of a key that is used as part of a hotstring, meaning AHK's built-in hotstring system is hamstrung by limitations that are no longer relevant.
So I started to write my own.
The ultimate goal is to provide on-screen cues to help guide the user through the hotstrings. ie if you have two hotstrings "aaa", "abc" and "aac", then typing "a" should show all three, if you type "aa" it should show "aaa" and "aac" etc, along with showing a hint as to what these hotstrings actually do.
In addition to hotstrings, my thought was to write my own equivalent of AHK's
Input command for AHI, so you could do keyboard-specific input of arbitrary text.
For example, let's say you are playing a flight simulator and want to control the angle of the flaps.
You would have a hotstring FL, which would then trigger an input, which would wait for you to type a number and hit ENTER.
So to set flaps to 50% would be
FL50<ENTER>
This would also transfer to other use-cases, eg in an art package you could have
OP75<ENTER> to set Opacity to 75%, or even maybe
<F1>ARIAL<ENTER> to set font to Arial
I managed to get *something* working, but I kind of lack inspiration to progress, so I am posting it here in the hopes that maybe some of the community can come up with some ideas as to how it should work.
This demo has two hotstrings
VO<NUMBERS><ENTER> = Set Volume
BR<TEXT><ENTER> = Launch browser, <TEXT> is looked up in the
browserChoices array.
Code: Select all
#SingleInstance force
#Persistent
#include Lib\AutoHotInterception.ahk
OutputDebug DBGVIEWCLEAR
global AHI := new AutoHotInterception()
global keyboardId := AHI.GetKeyboardId(0x413C, 0x2107, 1)
sm := new SequenceManager(Func("CommandBufferChange"))
sm.Add("Browse", ["b", "r"], "Launches a Browser to common locations", Func("BrowserStart"))
sm.Add("Volume", ["v", "o"], "Sets the volume to the specified level", Func("VolumeStart"))
global browserChoices := {goo: "http://google.com"}
return
^Esc::
Exit()
return
RenderSeqence(seq){
global sm
if (!seq.Length()){
return ""
}
for i, key in seq {
out .= sm.KeyNames[key] " "
}
return out
}
ToolTip(text := "", dur := -1){
ToolTip % text
if (dur != -1)
SetTimer, TooltipEnd, % - dur
}
TooltipEnd:
ToolTip
return
CommandBufferChange(buf, partialMatches){
global sm
commandStr := RenderSeqence(buf)
if (commandStr == ""){
ToolTip()
return
}
for name, command in partialMatches {
partialMatchStr .= name ": [ " RenderSeqence(command.Seq) " ] : " command.Description "`n"
}
ToolTip("Partial Matches:`n " partialMatchStr " `nBuffer:`n" commandStr)
}
BrowserStart(){
global sm
sm.EnableInputMode(Func("BrowserEnd"), Func("BroswerBufferChange"))
ToolTip("Browser Choice:`n ")
}
BroswerBufferChange(buf){
global sm, browserChoices
text := sm.GetTextInput()
for name, url in browserChoices {
if (InStr(name, text, flase, 1)){
partialMatches .= name ": " url "`n"
}
}
ToolTip("Partial Matches:`n" partialMatches "`n`nBuffer:`n" RenderSeqence(buf))
}
BrowserEnd(){
global sm, browserChoices
text := sm.GetTextInput()
if (browserChoices.HasKey(text)){
run % browserChoices[text]
}
ToolTip()
}
VolumeStart(){
global sm
sm.EnableInputMode(Func("VolumeEnd"), Func("VolumeBufferChange"))
ToolTip("Input Volume: `n ")
}
VolumeEnd(){
global sm
text := sm.GetNumberInput()
ToolTip("Set Volume: " text, 500)
SoundSet, % text
}
VolumeBufferChange(buf){
global sm
if (!buf.Length()){
ToolTip()
return
}
for i, key in buf {
out .= sm.KeyNames[key] " "
}
ToolTip("Input Volume:`n" out)
}
Exit(){
SoundBeep, 500, 250
ExitApp
}
; ===============================================================
class SequenceManager {
sequenceArray := {} ; A store of all the sequences
sequencesByLength := {} ; A lookup table of sequences, by length of sequence
longestSequence := 0 ; Length of the longest Sequence
shortestSequence := -1 ; Length of the shortest Sequence
textInputMode := 0 ; 0 = Normal (Command) mode, 1 = Input mode
commandBuffer := [] ; When in Command Mode, the characters in the command buffer
commandBufferUpdateCallback := 0 ; The callback for changes in command mode
commandBufferPartialMatches := {} ; When building up a command, the commands which partially match
KeyNames := {}
NumberCodes := {11: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 7, 9: 8, 10: 9, 12: "-", 13: "+" ; Number strip
, 82: 0, 79: 1, 80: 2, 81: 3, 75: 4, 76: 5, 77: 6, 71: 7, 72: 8, 73: 9, 74: "-", 78: "+"} ; Numpad
LetterCodes := {16:1, 17:1, 18:1, 19:1, 20:1, 21:1, 22:1, 23:1, 24:1, 25:1 ; top row
, 30:1, 31:1, 32:1, 33:1, 34:1, 35:1, 36:1, 37:1, 38:1 ; middle row
,44:1, 45:1, 46:1, 47:1, 48:1, 49:1, 50:1} ; bottom row
BackSpaceCodes := {14:1} ; Backspace
OkCodes := {28:1, 284:1} ; Enter / NumpadEnter
CancelCodes := {1:1} ; Esc
__New(commandBufferUpdateCallback := 0){
this.commandBufferUpdateCallback := commandBufferUpdateCallback
this.CreateHotkeys()
}
Add(name, _seq, desc, callback){
seq := []
for i, n in _seq {
code := GetKeySC(n)
if (!code){
MsgBox % "Unknown key " n
ExitApp
}
seq.Push(code)
}
sequence := new this.Sequence(name, seq, desc, callback)
this.sequenceArray[name] := sequence
max := seq.Length()
if (!this.sequencesByLength.HasKey(max)){
this.sequencesByLength[max] := {}
}
this.sequencesByLength[max][name] := sequence
if (max > this.longestSequence){
this.longestSequence := max
} else if (this.shortestSequence == -1 || max < this.shortestSequence){
this.shortestSequence := max
}
}
EnableInputMode(callback, inputBufferUpdateCallback := 0){
this.SetInputState(1)
this.InputCallback := callback
this.inputBufferUpdateCallback := inputBufferUpdateCallback
}
GetTextInput(){
out := ""
for i, code in this.InputBuffer {
printable := this.LookupLetter(code)
if (printable == -1){
return "ERROR"
}
out .= printable
}
return out
}
GetNumberInput(){
minus := 0
start := 0
max := this.InputBuffer.Length()
out := ""
if (this.NumberCodes[this.InputBuffer[1]] == "-"){
out := "-"
start++
}
Loop % max - start {
code := this.InputBuffer[A_Index + start]
num := this.LookupNumber(code)
if (num == -1){
return "ERROR"
}
out .= num
}
return out
}
LookupNumber(code){
if (this.NumberCodes.HasKey(code)){
return this.NumberCodes[code]
}
return -1
}
LookupLetter(i){
if (this.LetterCodes.HasKey(i)){
code := Format("{:x}", i)
return GetKeyName("SC" code)
}
return -1
}
SetInputState(state){
this.Log("Setting Input state to " state)
this.textInputMode := state
if (state){
this.InputBuffer := []
}
}
InputEvent(code, state){
if (!state)
return
if (this.textInputMode){
if (code == 0x1c || code == 0x11c){
;~ this.Log("ENTER")
this.SetInputState(0)
fn := this.InputCallback
SetTimer, % fn, -0
;~ this.InputCallback.Call(this.InputBuffer)
} else {
this.Log("Adding " code " to buffer")
this.InputBuffer.Push(code)
if (this.inputBufferUpdateCallback != 0){
this.inputBufferUpdateCallback.Call(this.InputBuffer)
}
}
} else {
if (this.BackSpaceCodes.HasKey(code)){
this.commandBuffer.Pop(code)
} else if (this.CancelCodes.HasKey(code)){
this.commandBuffer := []
} else {
this.commandBuffer.Push(code)
name := this.FindMatchingSequence()
if (name != -1){
this.commandBuffer := []
fn := this.sequenceArray[name].Callback
SetTimer, % fn, -0
}
}
this.CommandBufferUpdate()
}
this.Log("IO Event: Code: " code ", Key: " this.KeyNames[code])
}
FindMatchingSequence(){
this.commandBufferPartialMatches := {}
max := this.commandBuffer.Length()
;~ if (max >= this.shortestSequence){
Loop % this.longestSequence {
if (!this.sequencesByLength.HasKey(A_Index))
continue
for name, sequence in this.sequencesByLength[A_Index] {
match := 1
for i, c in sequence.Seq {
if (i > max){
; Partial match
this.commandBufferPartialMatches[name] := sequence
match := 0
break
}
if (this.commandBuffer[i] != c){
match := 0
break
}
}
if (match)
return name
}
}
; Same number of characters in buffer as longest sequence, none matched
; Reset here?
;~ }
return -1
}
CommandBufferUpdate(){
if (this.commandBufferUpdateCallback != 0)
this.commandBufferUpdateCallback.Call(this.commandBuffer, this.commandBufferPartialMatches)
}
Log(text){
OutputDebug % "AHK| SequenceManager | " text
}
CreateHotkeys(){
this.KeyNames := {}
Loop 512 {
i := A_Index
code := Format("{:x}", i)
name := GetKeyName("SC" code)
if (name == "")
continue
this.KeyNames[i] := name
fn := this.InputEvent.Bind(this, i)
AHI.SubscribeKey(keyboardId, i, true, fn)
;~ fn := this.InputEvent.Bind(this, 1, code, name)
;~ hotkey, % "SC" code, % fn
}
}
class Sequence {
__New(name, seq, desc, callback){
this.Name := name
this.Seq := seq
this.Description := desc
this.Max := this.Seq.Length()
this.Callback := callback
}
Log(text){
OutputDebug % "AHK| Sequence " this.name " | " text
}
}
}