| ... | ... | @@ -2,7 +2,8 @@ |
| 2 | 2 | |
| 3 | 3 | // Chrome-style hold-to-quit. The quit accelerator is intercepted in |
| 4 | 4 | // before-input-event (which runs before the native menu accelerator), and the |
| 5 | | // app only quits once the shortcut has been held for QUIT_HOLD_DURATION_MS. |
| 5 | // app only quits after the shortcut has been held for QUIT_HOLD_DURATION_MS |
| 6 | // and released. |
| 6 | 7 | // A quick tap just shows the renderer's "Hold to Quit" hint, and a second tap |
| 7 | 8 | // within QUIT_DOUBLE_TAP_MS quits immediately. Quitting from the application |
| 8 | 9 | // menu itself is untouched and quits immediately. |
| ... | ... | @@ -10,11 +11,11 @@ export const QUIT_HOLD_DURATION_MS = 1200; |
| 10 | 11 | // A second quick tap of the shortcut is the user insisting: quit immediately. |
| 11 | 12 | export const QUIT_DOUBLE_TAP_MS = 500; |
| 12 | 13 | // "Still held" is proven by auto-repeat keydowns, not by the absence of a |
| 13 | | // release: macOS suppresses a letter's keyUp while the command key is down, so |
| 14 | | // a tap's release can go completely unseen and a release-based timer would |
| 15 | | // quit anyway. The press is treated as released once no key event has arrived |
| 16 | | // for QUIT_HOLD_RELEASE_GRACE_MS past the hold duration. Keyboards with |
| 17 | | // auto-repeat disabled cannot hold-to-quit and fall back to the menu's Quit. |
| 14 | // release: macOS suppresses a letter keyUp while the command key is down, so a |
| 15 | // tap release can go completely unseen and a release-based timer would quit |
| 16 | // anyway. Once held, quitting waits for Q keyUp or a quiet grace period after |
| 17 | // modifier keyUp so repeats cannot reach the next app. Keyboards with |
| 18 | // auto-repeat disabled fall back to the application menu Quit action. |
| 18 | 19 | export const QUIT_HOLD_RELEASE_GRACE_MS = 600; |
| 19 | 20 | |
| 20 | 21 | export type QuitHoldState = "down" | "up"; |
| ... | ... | @@ -42,8 +43,9 @@ export function makeQuitHoldHandler( |
| 42 | 43 | const modifierKey = options.platform === "darwin" ? "meta" : "control"; |
| 43 | 44 | let watchdog: NodeJS.Timeout | undefined; |
| 44 | 45 | let holding = false; |
| 45 | | // Set once isEnabled resolves true; auto-repeats may only quit when armed. |
| 46 | // Set once isEnabled resolves true; auto-repeats may only complete the hold when armed. |
| 46 | 47 | let armed = false; |
| 48 | let quitOnRelease = false; |
| 47 | 49 | let heldSince = 0; |
| 48 | 50 | let lastPressAt = 0; |
| 49 | 51 | // Incremented on every new press and every release/quit so a pending |
| ... | ... | @@ -60,14 +62,16 @@ export function makeQuitHoldHandler( |
| 60 | 62 | |
| 61 | 63 | const release = () => { |
| 62 | 64 | if (!holding) return; |
| 65 | const shouldNotify = armed || quitOnRelease; |
| 63 | 66 | generation += 1; |
| 64 | 67 | holding = false; |
| 65 | 68 | armed = false; |
| 69 | quitOnRelease = false; |
| 66 | 70 | clearWatchdog(); |
| 67 | | options.notify("up"); |
| 71 | if (shouldNotify) options.notify("up"); |
| 68 | 72 | }; |
| 69 | 73 | |
| 70 | | // Dismisses the overlay first: if the quit is cancelled downstream the |
| 74 | // Dismisses any overlay first: if the quit is cancelled downstream the |
| 71 | 75 | // renderer must not be left with a stuck "Hold to Quit" hint. |
| 72 | 76 | const quitNow = () => { |
| 73 | 77 | release(); |
| ... | ... | @@ -77,11 +81,27 @@ export function makeQuitHoldHandler( |
| 77 | 81 | return (event, input) => { |
| 78 | 82 | const key = input.key.toLowerCase(); |
| 79 | 83 | if (input.type === "keyUp") { |
| 80 | | if (key === "q" || key === modifierKey) release(); |
| 84 | if (key === "q") { |
| 85 | const shouldQuit = quitOnRelease; |
| 86 | release(); |
| 87 | if (shouldQuit) options.quit(); |
| 88 | } else if (key === modifierKey) { |
| 89 | if (!quitOnRelease) { |
| 90 | release(); |
| 91 | } else { |
| 92 | watchdog = setTimeout(quitNow, QUIT_HOLD_RELEASE_GRACE_MS); |
| 93 | } |
| 94 | } |
| 81 | 95 | return; |
| 82 | 96 | } |
| 83 | 97 | if (input.type !== "keyDown") return; |
| 84 | 98 | |
| 99 | if (quitOnRelease && input.isAutoRepeat && key === "q") { |
| 100 | event.preventDefault(); |
| 101 | clearWatchdog(); |
| 102 | return; |
| 103 | } |
| 104 | |
| 85 | 105 | const modifierDown = options.platform === "darwin" ? input.meta : input.control; |
| 86 | 106 | if (!modifierDown || input.alt || input.shift || key !== "q") { |
| 87 | 107 | // Any other key (or an extra modifier) pressed mid-hold breaks the |
| ... | ... | @@ -101,7 +121,9 @@ export function makeQuitHoldHandler( |
| 101 | 121 | |
| 102 | 122 | if (input.isAutoRepeat) { |
| 103 | 123 | if (armed && Date.now() - heldSince >= QUIT_HOLD_DURATION_MS) { |
| 104 | | quitNow(); |
| 124 | armed = false; |
| 125 | quitOnRelease = true; |
| 126 | clearWatchdog(); |
| 105 | 127 | } |
| 106 | 128 | return; |
| 107 | 129 | } |
| ... | ... | @@ -121,7 +143,6 @@ export function makeQuitHoldHandler( |
| 121 | 143 | const pressGeneration = generation; |
| 122 | 144 | holding = true; |
| 123 | 145 | heldSince = now; |
| 124 | | options.notify("down"); |
| 125 | 146 | void options.isEnabled().then( |
| 126 | 147 | (enabled) => { |
| 127 | 148 | if (generation !== pressGeneration) return; |
| ... | ... | @@ -131,6 +152,7 @@ export function makeQuitHoldHandler( |
| 131 | 152 | return; |
| 132 | 153 | } |
| 133 | 154 | armed = true; |
| 155 | options.notify("down"); |
| 134 | 156 | // No auto-repeat by then means the key was released (possibly with a |
| 135 | 157 | // suppressed keyUp) or repeat is disabled; either way, don't quit. |
| 136 | 158 | watchdog = setTimeout(() => { |