fix: command+tab would sometimes not bind correctly (closes #5653)
This commit is contained in:
1 parent
60cca89bb8
commit
c75aac585a
7 files changed
+238
-17
No files matched your search
@@ -53,6 +53,11 @@
|
||||
5E0DE2A0000000000000D102 /* WindowOrderResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0DE2A0000000000000D101 /* WindowOrderResolverTests.swift */; };
|
||||
5E2A571E000000000000A002 /* WindowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E2A571E000000000000A001 /* WindowState.swift */; };
|
||||
5E2A571E000000000000A003 /* WindowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E2A571E000000000000A001 /* WindowState.swift */; };
|
||||
5FC5980000000000000C0002 /* CGSSymbolicHotKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC5980000000000000C0001 /* CGSSymbolicHotKey.swift */; };
|
||||
5FC5980000000000000C0003 /* CGSSymbolicHotKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC5980000000000000C0001 /* CGSSymbolicHotKey.swift */; };
|
||||
5FC5980000000000000A0002 /* NativeHotkeyResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC5980000000000000A0001 /* NativeHotkeyResolver.swift */; };
|
||||
5FC5980000000000000A0003 /* NativeHotkeyResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC5980000000000000A0001 /* NativeHotkeyResolver.swift */; };
|
||||
5FC5980000000000000A0102 /* NativeHotkeyResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC5980000000000000A0101 /* NativeHotkeyResolverTests.swift */; };
|
||||
5E2A571E000000000000A202 /* ApplicationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E2A571E000000000000A201 /* ApplicationState.swift */; };
|
||||
5E2A571E000000000000A203 /* ApplicationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E2A571E000000000000A201 /* ApplicationState.swift */; };
|
||||
D04BAE2200000000ED004T11 /* SettingsSearchHighlightTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE2200000000ED004T12 /* SettingsSearchHighlightTests.swift */; };
|
||||
@@ -506,6 +511,9 @@
|
||||
BF0C8B5D1A9658E64F8ECB56 /* SystemScrollerStyleEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemScrollerStyleEvents.swift; sourceTree = "<group>"; };
|
||||
BF0C8B841E890CA0D68FFD86 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
BF0C8B94D7994AB9007F6391 /* SkyLight.framework.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SkyLight.framework.swift; sourceTree = "<group>"; };
|
||||
5FC5980000000000000C0001 /* CGSSymbolicHotKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSSymbolicHotKey.swift; sourceTree = "<group>"; };
|
||||
5FC5980000000000000A0001 /* NativeHotkeyResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeHotkeyResolver.swift; sourceTree = "<group>"; };
|
||||
5FC5980000000000000A0101 /* NativeHotkeyResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeHotkeyResolverTests.swift; sourceTree = "<group>"; };
|
||||
BF0C8BA452332236D972C60E /* ATShortcut.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ATShortcut.swift; sourceTree = "<group>"; };
|
||||
BF0C8C0FF1B21F5F7CE16B79 /* print_env.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = print_env.sh; sourceTree = "<group>"; };
|
||||
BF0C8C5B74E5DA2CC31E52F4 /* extract_l10n_strings.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = extract_l10n_strings.sh; sourceTree = "<group>"; };
|
||||
@@ -829,6 +837,8 @@
|
||||
D04BAE1F00000000ED001002 /* ShortcutEditor.swift */,
|
||||
69A2E38AF41E866D3CDE71BF /* OverrideClickResolver.swift */,
|
||||
4B4409E4B3D98C491503CE5D /* OverrideClickResolverTests.swift */,
|
||||
5FC5980000000000000A0001 /* NativeHotkeyResolver.swift */,
|
||||
5FC5980000000000000A0101 /* NativeHotkeyResolverTests.swift */,
|
||||
5F21C9DF2C6E94240091F72F /* ShortcutsWhenActiveSheet.swift */,
|
||||
5F21C9E12C6E94410091F72F /* AdditionalControlsSheet.swift */,
|
||||
);
|
||||
@@ -1268,6 +1278,7 @@
|
||||
BF0C83EE9730BCFA41D840A9 /* README.md */,
|
||||
BF0C8C5C0FF5D567BF190DE5 /* ApplicationServices.HIServices.framework.swift */,
|
||||
BF0C8B94D7994AB9007F6391 /* SkyLight.framework.swift */,
|
||||
5FC5980000000000000C0001 /* CGSSymbolicHotKey.swift */,
|
||||
);
|
||||
path = "api-wrappers";
|
||||
sourceTree = "<group>";
|
||||
@@ -1793,6 +1804,8 @@
|
||||
A053BAD6681629878E72B1F7 /* ProBadgeViewSegmentTests.swift in Sources */,
|
||||
DC8CF4F01C999497352736F1 /* OverrideClickResolverTests.swift in Sources */,
|
||||
DC06402457ECCE6C902FEAE4 /* OverrideClickResolver.swift in Sources */,
|
||||
5FC5980000000000000A0003 /* NativeHotkeyResolver.swift in Sources */,
|
||||
5FC5980000000000000A0102 /* NativeHotkeyResolverTests.swift in Sources */,
|
||||
5E1EC110000000000000C003 /* SelectionResolver.swift in Sources */,
|
||||
5E1EC110000000000000C102 /* SelectionResolverTests.swift in Sources */,
|
||||
5E5EA12C0000000000000003 /* SearchModeResolver.swift in Sources */,
|
||||
@@ -1805,6 +1818,7 @@
|
||||
5E0DE2A0000000000000D102 /* WindowOrderResolverTests.swift in Sources */,
|
||||
5E2A571E000000000000A003 /* WindowState.swift in Sources */,
|
||||
5E2A571E000000000000A203 /* ApplicationState.swift in Sources */,
|
||||
5FC5980000000000000C0003 /* CGSSymbolicHotKey.swift in Sources */,
|
||||
D04BAE2200000000ED004T11 /* SettingsSearchHighlightTests.swift in Sources */,
|
||||
D04BAE2200000000ED004003 /* SettingsSearchHighlight.swift in Sources */,
|
||||
D04BAE2200000000ED004T01 /* TableGroupView.swift in Sources */,
|
||||
@@ -1958,6 +1972,7 @@
|
||||
D04BADA3F2DAB820D6FC6E75 /* AppCenterApplication.m in Sources */,
|
||||
6E800CF06EE430DCCF9CE47B /* ObjCExceptionCatcher.m in Sources */,
|
||||
D04BAA16F21AC41A23DDA63D /* ControlsTab.swift in Sources */,
|
||||
5FC5980000000000000A0002 /* NativeHotkeyResolver.swift in Sources */,
|
||||
D04BAE1F00000000ED001001 /* ShortcutEditor.swift in Sources */,
|
||||
DD6771EE8ACE714C2599FEB7 /* OverrideClickResolver.swift in Sources */,
|
||||
D04BADE4DDF7597749FB13BD /* SettingsWindow.swift in Sources */,
|
||||
@@ -1996,6 +2011,7 @@
|
||||
BF0C8A49891371E4027E0641 /* MacroPreferences.swift in Sources */,
|
||||
BF0C8E366BC25171C0A3881C /* ApplicationServices.HIServices.framework.swift in Sources */,
|
||||
BF0C803A13C409E7F01FBCAB /* SkyLight.framework.swift in Sources */,
|
||||
5FC5980000000000000C0002 /* CGSSymbolicHotKey.swift in Sources */,
|
||||
BF0C80C69EFEF93C18539925 /* PreferencesMigrations.swift in Sources */,
|
||||
BF0C8A2F6E4C85A7D9B63B12 /* Throttler.swift in Sources */,
|
||||
AA0C8A100000000000000001 /* AXCallScheduler.swift in Sources */,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
/// Identifiers for the native macOS symbolic hotkeys AltTab cares about. Defined in this small
|
||||
/// both-targets file (rather than `SkyLight.framework.swift`, which is app-only) so kernels like
|
||||
/// `NativeHotkeyResolver` that surface this enum can compile in the unit-tests target. See
|
||||
/// `setNativeCommandTabEnabled` / `CGSSetSymbolicHotKeyEnabled` in `SkyLight.framework.swift` for the
|
||||
/// runtime side of toggling them.
|
||||
enum CGSSymbolicHotKey: Int, CaseIterable {
|
||||
case commandTab = 1
|
||||
case commandShiftTab = 2
|
||||
case commandKeyAboveTab = 6 // see keyAboveTabDependingOnInputSource
|
||||
}
|
||||
@@ -161,12 +161,6 @@ func CGSGetWindowLevel(_ cid: CGSConnectionID, _ wid: CGWindowID, _ level: Unsaf
|
||||
@_silgen_name("SLSRequestScreenCaptureAccess") @discardableResult
|
||||
func SLSRequestScreenCaptureAccess() -> UInt8
|
||||
|
||||
enum CGSSymbolicHotKey: Int, CaseIterable {
|
||||
case commandTab = 1
|
||||
case commandShiftTab = 2
|
||||
case commandKeyAboveTab = 6 // see keyAboveTabDependingOnInputSource
|
||||
}
|
||||
|
||||
/// enables/disables a symbolic hotkeys. These are system shortcuts such as command+tab or Spotlight
|
||||
/// it is possible to find all the existing hotkey IDs by using CGSGetSymbolicHotKeyValue on the first few hundred numbers
|
||||
/// note: the effect of enabling/disabling persists after the app is quit
|
||||
|
||||
@@ -747,19 +747,17 @@ class ControlsTab {
|
||||
KeyboardEvents.anyShortcutUsesEscape = shortcuts.values.contains { $0.shortcut.carbonKeyCode == kVK_Escape }
|
||||
}
|
||||
|
||||
/// Thin adapter over `NativeHotkeyResolver.resolve` — builds the snapshot inputs from the live
|
||||
/// shortcut registry and applies the resolver's verdict via the symbolic-hotkey API. See
|
||||
/// `NativeHotkeyResolverSpecs.md` for the kernel's invariants and #5653's root cause.
|
||||
static func toggleNativeCommandTabIfNeeded() {
|
||||
let nativeHotkeys: [CGSSymbolicHotKey: (Shortcut) -> Bool] = [
|
||||
.commandTab: { shortcut in shortcut.carbonModifierFlags == cmdKey && shortcut.carbonKeyCode == kVK_Tab },
|
||||
.commandShiftTab: { shortcut in CustomRecorderControlTestable.combinedModifiersMatch(shortcut.carbonModifierFlags, UInt32(cmdKey | shiftKey)) && shortcut.carbonKeyCode == kVK_Tab },
|
||||
.commandKeyAboveTab: { shortcut in shortcut.carbonModifierFlags == cmdKey && shortcut.carbonKeyCode == kVK_ANSI_Grave },
|
||||
]
|
||||
var overlappingHotkeys = shortcuts.values.compactMap { atShortcut in nativeHotkeys.first { $1(atShortcut.shortcut) }?.key }
|
||||
if overlappingHotkeys.contains(.commandTab) && !overlappingHotkeys.contains(.commandShiftTab) {
|
||||
overlappingHotkeys.append(.commandShiftTab)
|
||||
let snapshots = shortcuts.values.map { ShortcutSnapshot(modifiers: $0.shortcut.carbonModifierFlags, keyCode: $0.shortcut.carbonKeyCode) }
|
||||
let holdShortcutModifiers: [UInt32] = (0..<Preferences.holdShortcut.count).compactMap { i in
|
||||
shortcuts[Preferences.indexToName("holdShortcut", i)]?.shortcut.carbonModifierFlags
|
||||
}
|
||||
let nonOverlappingHotkeys: [CGSSymbolicHotKey] = Array(Set(nativeHotkeys.keys).symmetricDifference(Set(overlappingHotkeys)))
|
||||
setNativeCommandTabEnabled(false, overlappingHotkeys)
|
||||
setNativeCommandTabEnabled(true, nonOverlappingHotkeys)
|
||||
let result = NativeHotkeyResolver.resolve(shortcuts: snapshots, holdShortcutModifiers: holdShortcutModifiers)
|
||||
setNativeCommandTabEnabled(false, Array(result.disable))
|
||||
setNativeCommandTabEnabled(true, Array(result.enable))
|
||||
}
|
||||
|
||||
@objc static func shortcutChangedCallback(_ sender: NSControl) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import Carbon.HIToolbox.Events
|
||||
|
||||
/// Minimal data record for one configured shortcut, in the form the native-hotkey classifier needs.
|
||||
/// Primitive types so this file compiles in the unit-tests target (same trick as `WindowState`).
|
||||
struct ShortcutSnapshot: Equatable {
|
||||
let modifiers: UInt32 // shortcut.carbonModifierFlags
|
||||
let keyCode: UInt32 // shortcut.carbonKeyCode
|
||||
}
|
||||
|
||||
/// Decides which native macOS symbolic hotkeys (⌘⇥ / ⌘⇧⇥ / ⌘`) AltTab must disable vs (re-)enable
|
||||
/// to make room for the configured shortcuts. Pure kernel: takes the configured shortcuts plus the
|
||||
/// set of hold-shortcut modifier flags currently in effect, returns disjoint disable/enable sets.
|
||||
///
|
||||
/// Issue #5653: the previous in-place implementation in `ControlsTab.toggleNativeCommandTabIfNeeded`
|
||||
/// used `nativeHotkeys.first { … }`, which made the classification depend on Swift dictionary
|
||||
/// iteration order. When a single shortcut matched multiple predicates (e.g. ⌘⇥ matches both
|
||||
/// `.commandTab` exactly *and* `.commandShiftTab` via `combinedModifiersMatch` when a ⌘⇧
|
||||
/// hold-shortcut is present), `.first` would pick one and drop the other, intermittently leaving
|
||||
/// native ⌘⇥ enabled for an entire session. This kernel collects **all** matches per shortcut, so
|
||||
/// the result is deterministic and independent of map iteration order.
|
||||
enum NativeHotkeyResolver {
|
||||
static func resolve(shortcuts: [ShortcutSnapshot], holdShortcutModifiers: [UInt32])
|
||||
-> (disable: Set<CGSSymbolicHotKey>, enable: Set<CGSSymbolicHotKey>) {
|
||||
var disable = Set<CGSSymbolicHotKey>()
|
||||
for s in shortcuts {
|
||||
if matchesCommandTab(s) { disable.insert(.commandTab) }
|
||||
if matchesCommandShiftTab(s, holdShortcutModifiers) { disable.insert(.commandShiftTab) }
|
||||
if matchesCommandKeyAboveTab(s) { disable.insert(.commandKeyAboveTab) }
|
||||
}
|
||||
// binding ⌘⇥ should also suppress the native reverse switcher (⌘⇧⇥)
|
||||
if disable.contains(.commandTab) { disable.insert(.commandShiftTab) }
|
||||
let enable = Set(CGSSymbolicHotKey.allCases).subtracting(disable)
|
||||
return (disable, enable)
|
||||
}
|
||||
|
||||
// MARK: - Native-hotkey predicates
|
||||
|
||||
private static func matchesCommandTab(_ s: ShortcutSnapshot) -> Bool {
|
||||
s.modifiers == UInt32(cmdKey) && s.keyCode == UInt32(kVK_Tab)
|
||||
}
|
||||
|
||||
private static func matchesCommandShiftTab(_ s: ShortcutSnapshot, _ holdShortcutModifiers: [UInt32]) -> Bool {
|
||||
s.keyCode == UInt32(kVK_Tab) && combinedModifiersMatch(s.modifiers, UInt32(cmdKey | shiftKey), holdShortcutModifiers)
|
||||
}
|
||||
|
||||
private static func matchesCommandKeyAboveTab(_ s: ShortcutSnapshot) -> Bool {
|
||||
s.modifiers == UInt32(cmdKey) && s.keyCode == UInt32(kVK_ANSI_Grave)
|
||||
}
|
||||
|
||||
/// True iff some hold-shortcut modifier set turns `modifiers1` and `modifiers2` into the same
|
||||
/// effective combo when OR'd in. Mirrors `CustomRecorderControlTestable.combinedModifiersMatch`
|
||||
/// but takes hold-shortcut modifiers as an explicit parameter rather than reading
|
||||
/// `ControlsTab.shortcuts` globals — keeps the kernel pure.
|
||||
private static func combinedModifiersMatch(_ modifiers1: UInt32, _ modifiers2: UInt32, _ holdShortcutModifiers: [UInt32]) -> Bool {
|
||||
holdShortcutModifiers.contains { (($0 | modifiers1) == ($0 | modifiers2)) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# NativeHotkeyResolver — Specs
|
||||
|
||||
## Summary
|
||||
|
||||
AltTab triggers its switcher via a Carbon `RegisterEventHotKey`. A native macOS symbolic hotkey
|
||||
(⌘⇥ / ⌘⇧⇥ / ⌘`), when enabled, is consumed by the Dock/WindowServer **before** any app-level
|
||||
Carbon hotkey — so binding ⌘⇥ to AltTab requires disabling the corresponding native one.
|
||||
|
||||
`NativeHotkeyResolver.resolve` is the pure kernel for that decision: given the configured shortcuts
|
||||
(as `ShortcutSnapshot` value records) and the modifier flags of the active hold-shortcuts, it
|
||||
returns disjoint `disable` / `enable` sets over `CGSSymbolicHotKey`. `ControlsTab.toggleNativeCommandTabIfNeeded`
|
||||
is a thin adapter that builds the inputs from `ControlsTab.shortcuts` and applies the result via
|
||||
`setNativeCommandTabEnabled`.
|
||||
|
||||
The kernel encodes one invariant the previous in-place code got wrong (issue #5653): **a single
|
||||
shortcut can overlap multiple native predicates and must contribute to all of them.** ⌘⇥ matches
|
||||
`.commandTab` exactly *and* `.commandShiftTab` via `combinedModifiersMatch` whenever a hold-shortcut
|
||||
carries shift (the user's second-shortcut scenario). The previous `.first { … }` over a dictionary
|
||||
of predicates picked one and dropped the others, with the pick varying per process (Swift dictionary
|
||||
iteration order isn't stable across launches) — leaving native ⌘⇥ enabled for whole sessions.
|
||||
|
||||
## Behavior & edge cases
|
||||
|
||||
- **Collect, don't pick.** Every native predicate a shortcut matches contributes to `disable`; nothing
|
||||
is dropped. The `enable` set is the complement over `CGSSymbolicHotKey.allCases`.
|
||||
- **⌘⇥ pairing.** Disabling `.commandTab` implicitly disables `.commandShiftTab` too, so the native
|
||||
reverse switcher doesn't fire while AltTab owns ⌘⇥.
|
||||
- **No globals.** `combinedModifiersMatch` previously read `ControlsTab.shortcuts` to find the hold
|
||||
modifiers; the kernel takes them as an explicit `holdShortcutModifiers: [UInt32]` parameter, so the
|
||||
resolver is independent of any global state.
|
||||
- **Primitive value record.** `ShortcutSnapshot` uses `UInt32` for both modifiers and keycode (no
|
||||
ShortcutRecorder / `Shortcut` types) so the kernel file compiles in the unit-tests target.
|
||||
|
||||
## Test scenarios
|
||||
|
||||
Mirrors `NativeHotkeyResolverTests.swift` 1:1.
|
||||
|
||||
### A. Issue #5653 — overlapping ⌘⇥ + ⌘⇧⇥ + hold ⌘ + ⌘⇧
|
||||
- **testCommandTabAndCommandShiftTabBothDisableNativeSwitchers** — the user's stuck-session
|
||||
config; both `.commandTab` and `.commandShiftTab` must end up disabled, regardless of which
|
||||
predicate the ⌘⇥ snapshot was visited under first.
|
||||
- **testResolutionIsDeterministicAcrossRepeatedCalls** — repeated calls on the same inputs always
|
||||
return the same sets (no dependence on map iteration order).
|
||||
|
||||
### B. Single ⌘⇥ — still pairs with ⌘⇧⇥
|
||||
- **testCommandTabAloneAlsoDisablesReverseSwitcher** — binding ⌘⇥ alone still suppresses native
|
||||
⌘⇧⇥ via the pairing rule.
|
||||
|
||||
### C. ⌘` alone — disables only that hotkey
|
||||
- **testCommandKeyAboveTabAloneDisablesOnlyThatHotkey** — no cross-talk between Tab and grave-key
|
||||
predicates; only `.commandKeyAboveTab` is disabled.
|
||||
|
||||
### D. Default option config — no native switcher overlap
|
||||
- **testOptionTabDoesNotOverrideNativeSwitchers** — AltTab's default ⌥⇥ / hold ⌥ doesn't overlap
|
||||
any native command-tab hotkey, so every native hotkey stays enabled.
|
||||
|
||||
### E. Empty config — nothing to override
|
||||
- **testEmptyConfigReleasesAllNativeHotkeys** — defensive: no shortcuts ⇒ no native hotkey disabled.
|
||||
@@ -0,0 +1,86 @@
|
||||
import XCTest
|
||||
import Carbon.HIToolbox.Events
|
||||
|
||||
/// Pins `NativeHotkeyResolver.resolve` against the configurations that historically tripped
|
||||
/// issue #5653 (intermittent native ⌘⇥ override on launch).
|
||||
///
|
||||
/// Groups: A repro case (⌘⇥ + ⌘⇧⇥ with hold ⌘ + ⌘⇧) · B single ⌘⇥ pairing ·
|
||||
/// C ⌘\` only · D default option · E empty.
|
||||
final class NativeHotkeyResolverTests: XCTestCase {
|
||||
private func snap(_ mods: Int, _ key: Int) -> ShortcutSnapshot {
|
||||
ShortcutSnapshot(modifiers: UInt32(mods), keyCode: UInt32(key))
|
||||
}
|
||||
|
||||
// MARK: - A. Issue #5653 — ⌘⇥ + ⌘⇧⇥ with hold ⌘ + ⌘⇧
|
||||
|
||||
/// With both ⌘⇥ and ⌘⇧⇥ bound (and the matching ⌘ + ⌘⇧ hold-shortcuts present), both native
|
||||
/// switchers must be disabled — even though the ⌘⇥ snapshot matches *two* native predicates
|
||||
/// (`.commandTab` exact, `.commandShiftTab` via combined hold). The pre-fix `.first { … }`
|
||||
/// over the predicate dictionary would intermittently drop `.commandTab` here.
|
||||
func testCommandTabAndCommandShiftTabBothDisableNativeSwitchers() {
|
||||
let result = NativeHotkeyResolver.resolve(
|
||||
shortcuts: [snap(cmdKey, kVK_Tab), snap(cmdKey | shiftKey, kVK_Tab)],
|
||||
holdShortcutModifiers: [UInt32(cmdKey), UInt32(cmdKey | shiftKey)])
|
||||
XCTAssertEqual(result.disable, [.commandTab, .commandShiftTab])
|
||||
XCTAssertEqual(result.enable, [.commandKeyAboveTab])
|
||||
}
|
||||
|
||||
/// Within one process the result must be stable across repeated calls on identical inputs.
|
||||
/// (The original cross-process flakiness — different Swift dictionary hash seeds picking a
|
||||
/// different `.first` predicate per launch — was the user-visible bug.)
|
||||
func testResolutionIsDeterministicAcrossRepeatedCalls() {
|
||||
let shortcuts = [snap(cmdKey, kVK_Tab), snap(cmdKey | shiftKey, kVK_Tab)]
|
||||
let holds = [UInt32(cmdKey), UInt32(cmdKey | shiftKey)]
|
||||
let first = NativeHotkeyResolver.resolve(shortcuts: shortcuts, holdShortcutModifiers: holds)
|
||||
for _ in 0..<50 {
|
||||
let r = NativeHotkeyResolver.resolve(shortcuts: shortcuts, holdShortcutModifiers: holds)
|
||||
XCTAssertEqual(r.disable, first.disable)
|
||||
XCTAssertEqual(r.enable, first.enable)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - B. Single ⌘⇥ — still pairs with ⌘⇧⇥
|
||||
|
||||
/// With only ⌘⇥ bound, native ⌘⇧⇥ (reverse switcher) must also be disabled so it doesn't fire
|
||||
/// when the user presses shift while the switcher is open.
|
||||
func testCommandTabAloneAlsoDisablesReverseSwitcher() {
|
||||
let result = NativeHotkeyResolver.resolve(
|
||||
shortcuts: [snap(cmdKey, kVK_Tab)],
|
||||
holdShortcutModifiers: [UInt32(cmdKey)])
|
||||
XCTAssertEqual(result.disable, [.commandTab, .commandShiftTab])
|
||||
XCTAssertEqual(result.enable, [.commandKeyAboveTab])
|
||||
}
|
||||
|
||||
// MARK: - C. ⌘` alone — disables only that hotkey
|
||||
|
||||
/// Binding ⌘\` overrides the native "key above Tab" hotkey but leaves both switcher hotkeys
|
||||
/// alone. No cross-talk between Tab and grave key predicates.
|
||||
func testCommandKeyAboveTabAloneDisablesOnlyThatHotkey() {
|
||||
let result = NativeHotkeyResolver.resolve(
|
||||
shortcuts: [snap(cmdKey, kVK_ANSI_Grave)],
|
||||
holdShortcutModifiers: [UInt32(cmdKey)])
|
||||
XCTAssertEqual(result.disable, [.commandKeyAboveTab])
|
||||
XCTAssertEqual(result.enable, [.commandTab, .commandShiftTab])
|
||||
}
|
||||
|
||||
// MARK: - D. Default option config — no native switcher overlap
|
||||
|
||||
/// AltTab's default trigger (⌥⇥ / hold ⌥) doesn't overlap any native command-tab hotkey, so
|
||||
/// every native hotkey stays enabled — AltTab coexists with the system's native switcher.
|
||||
func testOptionTabDoesNotOverrideNativeSwitchers() {
|
||||
let result = NativeHotkeyResolver.resolve(
|
||||
shortcuts: [snap(optionKey, kVK_Tab)],
|
||||
holdShortcutModifiers: [UInt32(optionKey)])
|
||||
XCTAssertEqual(result.disable, [])
|
||||
XCTAssertEqual(result.enable, [.commandTab, .commandShiftTab, .commandKeyAboveTab])
|
||||
}
|
||||
|
||||
// MARK: - E. No shortcuts at all — nothing to override
|
||||
|
||||
/// Defensive: with no shortcuts configured, no native hotkey is disabled.
|
||||
func testEmptyConfigReleasesAllNativeHotkeys() {
|
||||
let result = NativeHotkeyResolver.resolve(shortcuts: [], holdShortcutModifiers: [])
|
||||
XCTAssertEqual(result.disable, [])
|
||||
XCTAssertEqual(result.enable, [.commandTab, .commandShiftTab, .commandKeyAboveTab])
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user