diff --git a/js/app.js b/js/app.js index 28a98cb..25191b0 100644 --- a/js/app.js +++ b/js/app.js @@ -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');