fix(hotkeys): detect a swallowed combo on key release, not after 10 s
"No combo captured" was both slow and wrong: the user HAD pressed a combo. We actually receive the Ctrl/Shift/Alt keydowns — only the final key of a combo owned by another app is swallowed by Windows. So "modifiers held, then all released, no key ever arrived" pinpoints the situation exactly. Reported on keyup, instantly, naming what was held: "No key registered after Ctrl+Shift. If you did press one, another app already owns that combo — pick a different one." The wording also holds if the user just tapped a modifier and let go. The 10 s timer stays as the fallback for a capture armed and abandoned with no key at all (it would otherwise swallow every keystroke until Esc), with a message that no longer contradicts the user. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7722,16 +7722,21 @@ async function init() {
|
||||
hotkeyPushSilent = true;
|
||||
Bridge.suspendHotkeys();
|
||||
|
||||
// A combo already owned by another app via RegisterHotKey is
|
||||
// swallowed by Windows — it is delivered to that app, never to us,
|
||||
// so the capture would wait forever with no explanation. It also
|
||||
// preventDefaults every keystroke while armed, so an abandoned
|
||||
// capture freezes the app's keyboard. Time out and say why.
|
||||
// Modifiers the user is currently holding. We DO receive the
|
||||
// Ctrl/Shift/Alt keydowns; only the final key of a combo owned by
|
||||
// another app is swallowed by Windows (routed to that app). So
|
||||
// "modifiers held, then all released, no key ever arrived" is a
|
||||
// precise signal that the combo is taken — reported on keyup, with
|
||||
// no wait. See onKeyUp.
|
||||
let heldMods = '';
|
||||
|
||||
// Fallback for "armed but nothing was pressed at all" (the user
|
||||
// walked away). Also matters because the capture preventDefaults
|
||||
// every keystroke while armed — an abandoned one would freeze the
|
||||
// app's keyboard until Esc.
|
||||
const timer = setTimeout(() => {
|
||||
finish(true);
|
||||
toast('No combo captured. If you did press one, another app ' +
|
||||
'already owns it — Windows routes it there and we never ' +
|
||||
'see it. Try a different combo.', 'warning');
|
||||
toast('Hotkey capture cancelled — no key was pressed.', 'warning');
|
||||
}, 10000);
|
||||
|
||||
function finish(restore) {
|
||||
@@ -7739,6 +7744,7 @@ async function init() {
|
||||
btn.dataset.capturing = '';
|
||||
btn.classList.remove('is-capturing');
|
||||
document.removeEventListener('keydown', onKey, true);
|
||||
document.removeEventListener('keyup', onKeyUp, true);
|
||||
if (restore) {
|
||||
btn.textContent = original;
|
||||
// Cancelled / timed out: nothing changed, so re-register the
|
||||
@@ -7753,7 +7759,17 @@ async function init() {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const captured = autofillCaptureFromEvent(e);
|
||||
if (captured === null) return; // still building
|
||||
if (captured === null) { // still building
|
||||
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) {
|
||||
const p = [];
|
||||
if (e.ctrlKey) p.push('Ctrl');
|
||||
if (e.altKey) p.push('Alt');
|
||||
if (e.shiftKey) p.push('Shift');
|
||||
if (e.metaKey) p.push('Win');
|
||||
heldMods = p.join('+');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (captured === 'cancel') { finish(true); return; }
|
||||
if (captured === 'invalid'){
|
||||
toast('Unsupported key — use a letter, digit or F-key', 'warning');
|
||||
@@ -7790,7 +7806,23 @@ async function init() {
|
||||
autofillPushHotkeys(); // re-register in Delphi
|
||||
saveServerSettings(); // sync to server (debounced)
|
||||
}
|
||||
// All modifiers released while still "building" = the key that
|
||||
// followed them never reached us, i.e. Windows handed the combo to
|
||||
// whichever app registered it. Say exactly that instead of leaving
|
||||
// the user staring at "Press combo…".
|
||||
function onKeyUp(e) {
|
||||
if (!heldMods) return;
|
||||
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
|
||||
const mods = heldMods;
|
||||
finish(true);
|
||||
// Worded so it holds whether they pressed a swallowed key or
|
||||
// just tapped a modifier and let go.
|
||||
toast('No key registered after ' + mods + '. If you did press one, ' +
|
||||
'another app already owns that combo — pick a different one.',
|
||||
'warning');
|
||||
}
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
document.addEventListener('keyup', onKeyUp, true);
|
||||
});
|
||||
}
|
||||
bindHotkeyCapture('#settingAutofillFullCombo', 'full');
|
||||
|
||||
Reference in New Issue
Block a user