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;
|
hotkeyPushSilent = true;
|
||||||
Bridge.suspendHotkeys();
|
Bridge.suspendHotkeys();
|
||||||
|
|
||||||
// A combo already owned by another app via RegisterHotKey is
|
// Modifiers the user is currently holding. We DO receive the
|
||||||
// swallowed by Windows — it is delivered to that app, never to us,
|
// Ctrl/Shift/Alt keydowns; only the final key of a combo owned by
|
||||||
// so the capture would wait forever with no explanation. It also
|
// another app is swallowed by Windows (routed to that app). So
|
||||||
// preventDefaults every keystroke while armed, so an abandoned
|
// "modifiers held, then all released, no key ever arrived" is a
|
||||||
// capture freezes the app's keyboard. Time out and say why.
|
// 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(() => {
|
const timer = setTimeout(() => {
|
||||||
finish(true);
|
finish(true);
|
||||||
toast('No combo captured. If you did press one, another app ' +
|
toast('Hotkey capture cancelled — no key was pressed.', 'warning');
|
||||||
'already owns it — Windows routes it there and we never ' +
|
|
||||||
'see it. Try a different combo.', 'warning');
|
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
function finish(restore) {
|
function finish(restore) {
|
||||||
@@ -7739,6 +7744,7 @@ async function init() {
|
|||||||
btn.dataset.capturing = '';
|
btn.dataset.capturing = '';
|
||||||
btn.classList.remove('is-capturing');
|
btn.classList.remove('is-capturing');
|
||||||
document.removeEventListener('keydown', onKey, true);
|
document.removeEventListener('keydown', onKey, true);
|
||||||
|
document.removeEventListener('keyup', onKeyUp, true);
|
||||||
if (restore) {
|
if (restore) {
|
||||||
btn.textContent = original;
|
btn.textContent = original;
|
||||||
// Cancelled / timed out: nothing changed, so re-register the
|
// Cancelled / timed out: nothing changed, so re-register the
|
||||||
@@ -7753,7 +7759,17 @@ async function init() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const captured = autofillCaptureFromEvent(e);
|
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 === 'cancel') { finish(true); return; }
|
||||||
if (captured === 'invalid'){
|
if (captured === 'invalid'){
|
||||||
toast('Unsupported key — use a letter, digit or F-key', 'warning');
|
toast('Unsupported key — use a letter, digit or F-key', 'warning');
|
||||||
@@ -7790,7 +7806,23 @@ async function init() {
|
|||||||
autofillPushHotkeys(); // re-register in Delphi
|
autofillPushHotkeys(); // re-register in Delphi
|
||||||
saveServerSettings(); // sync to server (debounced)
|
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('keydown', onKey, true);
|
||||||
|
document.addEventListener('keyup', onKeyUp, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
bindHotkeyCapture('#settingAutofillFullCombo', 'full');
|
bindHotkeyCapture('#settingAutofillFullCombo', 'full');
|
||||||
|
|||||||
Reference in New Issue
Block a user