fix: cmd key could be stuck after switching (closes #5650)

This commit is contained in:
lwouis
2026-05-22 18:53:59 +02:00
parent 910288d149
commit 3a48a13cb5
3 changed files with 4 additions and 31 deletions
-8
View File
@@ -307,13 +307,6 @@ class App: AppCenterApplication {
static func showUiOrCycleSelection(_ shortcutIndex: Int, _ forceDoNothingOnRelease_: Bool) {
let session = SwitcherSession.current ?? {
let new = SwitcherSession()
// Read the cached value rather than querying NSWorkspace (which is a blocking IPC)
// on this hot path. AX activation notifications can be in-flight when the user invokes
// the switcher right after activating an app, so this can be momentarily stale. In that
// race, the modifier release is redelivered to the previously-foreground app instead of
// the real initial app a spurious but benign modifier-state update for that app, and
// the destination is still shielded from receiving the release.
new.initialPid = Applications.frontmostPid
SwitcherSession.current = new
return new
}()
@@ -328,7 +321,6 @@ class App: AppCenterApplication {
}
session.isFirstSummon = false
session.shortcutIndex = shortcutIndex
session.holdMask = ControlsTab.shortcuts[Preferences.indexToName("holdShortcut", shortcutIndex)]?.shortcut.modifierFlags ?? []
// Hide instantly so the rebuild for a different shortcut (Appearance change, layout
// recalc) is invisible. `TilesPanel.show()` flips alpha back to 1 once everything is
// in its final state. No-op on first summon (panel was orderOut'd with alpha=0).
+4 -14
View File
@@ -22,23 +22,13 @@ class KeyboardEvents {
case .flagsChanged:
// TODO: it would be great to shortcut matching and trigger on the background thread
// it would enable us to set App.shared.isBeingUsed here, and could stop tasks on main when they check the flag
let modifiers = NSEvent.ModifierFlags(rawValue: UInt(cgEvent.flags.rawValue))
// When the hold-shortcut mask transitions out of fully held during a session,
// redeliver the real event to the initial app's PID and absorb the system-routed
// copy so the destination app doesn't receive the release. Posting first (before
// the main-thread async that drives focus switching) gives A's run loop a head
// start over the focus change.
var absorb = false
if let session = SwitcherSession.current, let pid = session.initialPid,
!session.holdMask.isEmpty, !modifiers.isSuperset(of: session.holdMask),
let copy = cgEvent.copy() {
copy.postToPid(pid)
absorb = true
}
DispatchQueue.main.async {
let modifiers = NSEvent.ModifierFlags(rawValue: UInt(cgEvent.flags.rawValue))
// TODO: ideally, we want to absorb all modifier keys except holdShortcut
// it was pressed down before AltTab was triggered, so we should let the up event through
handleKeyboardEvent(nil, nil, nil, modifiers, false)
}
return absorb ? nil : Unmanaged.passUnretained(cgEvent)
return Unmanaged.passUnretained(cgEvent)
case .keyDown:
// Issue #5585. Esc only absorb when AltTab is using it and a shortcut binds it. cghid is
// the earliest tap point; absorbing here preempts macOS 26 Game Overlay's hook on ``.
-9
View File
@@ -21,13 +21,4 @@ final class SwitcherSession {
var hoveredIndex: Int?
var selectedTarget: String?
var searchQuery: String = ""
/// PID of the app that was frontmost when this session started. The `cgEventHandler`
/// in `KeyboardEvents` redelivers the hold-modifier release event to this PID via
/// `CGEventPostToPid`, so the initial app sees a release matching the press it saw
/// when the user invoked the switcher.
var initialPid: pid_t?
/// Modifier mask of the active hold shortcut. The trigger event is the `flagsChanged`
/// where the current modifiers no longer fully satisfy this mask.
var holdMask: NSEvent.ModifierFlags = []
}