How do I create a hotkey for a sequence of inputs while holding Ctrl, e.g. {Ctrl Down}ec{Ctrl Up}?

Solution 1:

Finally figured it out.

tl;dr

Replace ^e with first desired keystroke. Replace 3 with ASCII index of second desired keystroke. This enforces that Ctrl is held through both keystrokes, else cancels.

~$Ctrl UP::
    ChordIsBroken := True
    Return
^e::
    ChordIsBroken := False
    Input, OutputVar, L1 M
    If (!ChordIsBroken && Asc(OutputVar) = 3)
    {
        MsgBox "Hello, World!"
    }
    Else
    {
        SendInput %OutputVar%
    }
    Return

To adapt this to Shift instead of Ctrl, you'd have to replace the Ctrls, remove the M, and do a simpler comparison like OutputVar = C instead of the Asc(OutputVar) = 3. Not sure how to extend this to Alt and Win but you might have to try L2 or something of the sort.

Explanation

Input seemed like an obvious place to begin. Input asks AHK to wait for user input, e.g.

^e::
    Input, OutputVar, L1        ; "L1" means wait for 1 keystroke then continue.
    If (OutputVar = "c")
    {
        MsgBox, "Hello, World!"
    }
    Return

The message box above triggers on CtrlE then C. But we're looking for CtrlC, so let's fix this:

^e::
    Input, OutputVar, L1 M      ; "M" allows storage of modified keystrokes (^c).
    If (Asc(OutputVar) = 3)     ; ASCII character 3 is ^c.
    {
        MsgBox "Hello, World!"
    }
    Return

Now we've got a message box on pressing CtrlE then CtrlC. But there's a problem with this: the message box triggers even when I release Ctrl between the two keystrokes. So, how do we detect essentially a {Ctrl Up} mid-input? You can't merely check on entrance—

^e::
    if (!GetKeyState("Ctrl"))
    {
        Return
    }
    Input, OutputVar, L1 M 
    ; ...

—nor can you merely check after input—

^e::
    Input, OutputVar, L1 M 
    if (!GetKeyState("Ctrl"))
    {
        Return
    }
    ; ...

—nor can you even get away with doing both, because no matter what, you'll miss the {Ctrl Up} while blocking for input.

Then I looked into documentation on Hotkey for inspiration. The custom combination operator, &, seemed promising. But unfortunately,

^e & ^c::
    ; ...

caused a compilation error; apparently the & is for combining unmodified keystrokes, only.

Finally, it was up to UP, and that's where I finally made the breakthrough. I redefined Ctrl's UP to set a toggle that would prevent the message box from triggering!

$Ctrl::Send {Ctrl Down}     ; The $ prevents an infinite loop. Although this
$Ctrl UP::                  ; line seems redundant, it is in fact necessary.
    ChordIsBroken := True   ; Without it, Ctrl becomes effectively disabled.
    Send {Ctrl Up}
    Return
^e::
    ChordIsBroken := False
    Input, OutputVar, L1 M
    If (!ChordIsBroken && Asc(OutputVar) = 3)
    {
        MsgBox "Hello, World!"
    }
    Return

Now, when I press CtrlE, then release the Ctrl, and press CtrlC, nothing happens, as expected!

There was one last thing to fix. On "cancellation" (a "broken chord"), I wanted all keystrokes to return to normal. But in the code above, the Input would have to "eat" a keystroke before returning, regardless a broken chord or an irrelevant secondary keystroke. Adding an Else case nicely resolves this:

    Else
    {
        SendInput %OutputVar%
    }

So, there you have it—"chords" in AutoHotkey. (Although, I wouldn't exactly call this a "chord." More like a melody, with a bass line ;-)


@hippibruder generously points out that I can avoid defining $Ctrl:: by using ~ to make $Ctrl UP:: non-blocking. This allows some simplification! (See tl;dr section at top for final result.)


One more thing. If, perchance, upon "cancellation" (a "broken chord"), you'd like to issue the first keystroke, i.e. CtrlE on its own, simply add that in the Else block,

    Else
    {
        SendInput ^e
        SendInput %OutputVar%
    }

and don't forget to change the hotkey to

$^e::

to avoid an infinite loop.

Solution 2:

I don't think there is build-in support for key chords in AHK. One way to detect these would be to register a hotkey for the first key in the chord (^e) and then use the Input-Command to detect the next keys.

; Tested with AHK_L U64 v1.1.14.03 (and Visual Studio 2010)
; This doesn't block the input. To block it remove '~' from the hotkey and 'V' from Input-Cmd
~^e::
  ; Input-Cmd will capture the next keyinput as its printable representation. 
  ; (i.e. 'Shift-a' produces 'A'. 'a' produces 'a'. 'Ctrl-k' produces nothing printable. This can be changed with 'M' option. Maybe better approch; See help) 
  ; Only the next, because of 'L1'. (Quick-Fail; Not necessary)
  ; To detect 'c' and 'u' with control pressed I used them as EndKeys.
  ; If a EndKey is pressed the Input-Cmd will end and save the EndKey in 'ErrorLevel'.
  Input, _notInUse, V L1 T3, cu

  ; Return if Input-Cmd was not terminated by an EndKey
  ; or 'Control' is no longer pressed. (It would be better if Input-Cmd would be also terminated by a {Ctrl Up}. I don't know if that is possible)
  if ( InStr(ErrorLevel, "Endkey:") != 1
    || !GetKeyState("Control") )
    {
    return
    }

  ; Extract the EndKey-Name from 'ErrorLevel' (ErrorLevel == "Endkey:c")
  key := SubStr(ErrorLevel, 8)    
  if ( InStr(key, "c") == 1 )
    {
    TrayTip,, ^ec
    }
  else if ( InStr(key, "u") == 1 )
    {
    TrayTip,, ^eu
    }
  else
    {
    MsgBox, wut? key="%key%"
    }
return