From 09ca4c30b4d9a6b35d2fb36e5a0362cb76160052 Mon Sep 17 00:00:00 2001 From: Dylan Deverill Date: Wed, 16 Sep 2026 21:51:56 -0700 Subject: [PATCH] Let Escape be captured as a key-combo binding (#105) Escape used to be intercepted globally as the remap window's cancel affordance, so pressing it while listening just exited capture instead of binding anything. Now it only dismisses the unsaved-changes modal or the game picker; while a row is listening, Escape falls through to the normal capture path (alone or as part of a combo), and the VK table gained the missing entry so it resolves to a real binding. Co-Authored-By: Claude Sonnet 5 --- src/app/KeyInput.cpp | 4 ++-- src/app/RemapWindow.cpp | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/app/KeyInput.cpp b/src/app/KeyInput.cpp index c4e127a..184ef3b 100644 --- a/src/app/KeyInput.cpp +++ b/src/app/KeyInput.cpp @@ -8,8 +8,7 @@ namespace { struct JsCodeToVk { const char* code; uint16_t vk; }; // KeyboardEvent.code values are physical positions, so a binding made on one -// keyboard layout means the same physical key on another. Escape is absent on -// purpose — the remap window uses it to cancel listening. +// keyboard layout means the same physical key on another. const JsCodeToVk kCodeMap[] = { // Letters {"KeyA",'A'},{"KeyB",'B'},{"KeyC",'C'},{"KeyD",'D'},{"KeyE",'E'},{"KeyF",'F'}, @@ -29,6 +28,7 @@ const JsCodeToVk kCodeMap[] = { {"Backspace", VK_BACK}, {"Delete", VK_DELETE}, {"Insert", VK_INSERT}, + {"Escape", VK_ESCAPE}, // Navigation {"ArrowUp", VK_UP}, diff --git a/src/app/RemapWindow.cpp b/src/app/RemapWindow.cpp index 2f6542a..4f14146 100644 --- a/src/app/RemapWindow.cpp +++ b/src/app/RemapWindow.cpp @@ -1336,18 +1336,20 @@ document.addEventListener('auxclick',function(e){ }); document.addEventListener('keydown',function(e){ - // Escape stays the cancel affordance, so it is deliberately not bindable. - // Innermost dismissable thing first: the modal is the only one that can sit - // on top of the others. + // Escape closes whichever overlay sits on top of the page — innermost + // first, and the modal is the only one that can sit on top of the other. + // Neither overlay is up while a row is listening, so in that case Escape + // falls through to the capture logic below and binds like any other key + // (issue #105) — the row's own Cancel button is how listening is aborted. if(e.key==='Escape'){ if(pendingAction!==null||document.getElementById('modal-backdrop').style.display==='flex'){ resolveModal('cancel'); - } else if(comboOpen){ + return; + } + if(comboOpen){ closeCombo(); - } else { - cancelListening(); + return; } - return; } // The search box is a real text field — typing in it must not be swallowed // by the binding-capture handler below.