fix: works without screen-recording permissions (closes #3819)

This commit is contained in:
lwouis committed 2024-11-10 13:10:20 +01:00
1 parent d430f835a4
commit f7de2bb6d9
7 files changed
+66 -9

No files matched your search

+48 -5
View File
@@ -1,17 +1,60 @@
import Foundation
class MissionControl {
static func isActive() -> Bool {
// when Mission Control is active, the Dock process spawns some windows. We observe this side-effect and infer
private static var state_ = MissionControlState.inactive
private static var axObserver: AXObserver?
private static var axUiElement: AXUIElement?
static func state() -> MissionControlState {
if #available(macOS 12.0, *) {
return state_
} else {
return isActive() ? .showAllWindows : .inactive
}
}
static func setState(_ state: MissionControlState) {
state_ = state
}
// on macOS < 12, this is the way we used to guess if Mission Control is active
// on macOS >= 12, we listen to private notifications, which is accurate
// when Mission Control is active, the Dock process spawns some windows. We observe this side-effect and infer
private static func isActive() -> Bool {
for window in CGWindow.windows(.optionOnScreenOnly) {
// ownerName == "Dock" && title == nil is a sign that Mission Control may be active
if window.ownerName() == "Dock" && window.title() == nil
// layer == 500 can be a false positive when a user drags a file from a Dock folder
// see https://github.com/lwouis/alt-tab-macos/issues/706
&& window.layer() != 500 {
// layer == 500 can be a false positive when a user drags a file from a Dock folder
// see https://github.com/lwouis/alt-tab-macos/issues/706
&& window.layer() != 500 {
return true
}
}
return false
}
static func observe(_ dockPid: pid_t) {
if #available(macOS 12.0, *) {
axUiElement = AXUIElementCreateApplication(dockPid)
AXObserverCreate(dockPid, axObserverCallbackDock, &axObserver)
// are we sure we always get a non-nil axObserver?
for notification in MissionControlState.allCases {
retryAxCallUntilTimeout {
try axUiElement!.subscribeToNotification(axObserver!, notification.rawValue, nil)
}
}
CFRunLoopAddSource(BackgroundWork.missionControlThread.runLoop, AXObserverGetRunLoopSource(axObserver!), .defaultMode)
} else {
// we could handle macOS < 12 here like yabai does. However, they poll with ax calls until they notice Mission Control stops
// this takes up ressources when Mission Control is open. If the user keeps it open for a few hours, this would accelerate battery usage
// SLSRegisterConnectionNotifyProc(g_connection, connection_handler, 1204, NULL);
// then listen every 0.1f * NSEC_PER_SEC for layer == 18 and owner = "Dock"
// when found, mission control is not active anymore
}
}
}
fileprivate let axObserverCallbackDock: AXObserverCallback = { _, element, notificationName, _ in
logger.d(notificationName, element)
MissionControl.setState(MissionControlState(rawValue: notificationName as String)!)
}
+8
View File
@@ -207,6 +207,14 @@ let kAXStatusLabelAttribute = "AXStatusLabel"
// for some reason, these attributes are missing from AXRoleConstants
let kAXDocumentWindowSubrole = "AXDocumentWindow"
// mission control private notifications
enum MissionControlState: String, CaseIterable {
case showAllWindows = "AXExposeShowAllWindows"
case showFrontWindows = "AXExposeShowFrontWindows"
case showDesktop = "AXExposeShowDesktop"
case inactive = "AXExposeExit"
}
enum CGSSymbolicHotKey: Int, CaseIterable {
case commandTab = 1
case commandShiftTab = 2
+1
View File
@@ -76,6 +76,7 @@ class ATShortcut {
}
func executeAction(_ isARepeat: Bool) {
logger.i(id)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
KeyRepeatTimer.isARepeat = isARepeat
+3
View File
@@ -47,6 +47,9 @@ class Applications {
static func addRunningApplications(_ runningApps: [NSRunningApplication]) {
runningApps.forEach {
if $0.bundleIdentifier == "com.apple.dock" {
MissionControl.observe($0.processIdentifier)
}
if isActualApplication($0) {
Applications.list.append(Application($0))
}
+2
View File
@@ -10,6 +10,7 @@ class BackgroundWork {
static var mouseEventsThread: BackgroundThreadWithRunLoop!
static var systemPermissionsThread: BackgroundThreadWithRunLoop!
static var repeatingKeyThread: BackgroundThreadWithRunLoop!
static var missionControlThread: BackgroundThreadWithRunLoop!
// we cap concurrent tasks to .processorCount to avoid thread explosion on the .global queue
static let globalSemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.processorCount)
@@ -25,6 +26,7 @@ class BackgroundWork {
accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread", .userInteractive)
mouseEventsThread = BackgroundThreadWithRunLoop("mouseEventsThread", .userInteractive)
repeatingKeyThread = BackgroundThreadWithRunLoop("repeatingKeyThread", .userInteractive)
missionControlThread = BackgroundThreadWithRunLoop("missionControlThread", .userInteractive)
}
static func startSystemPermissionThread() {
+1
View File
@@ -132,6 +132,7 @@ class KeyboardEvents {
@discardableResult
fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: UInt32?, _ isARepeat: Bool) -> Bool {
logger.d(id, shortcutState, keyCode, modifiers, isARepeat)
var someShortcutTriggered = false
for shortcut in ControlsTab.shortcuts.values {
if shortcut.matches(id, shortcutState, keyCode, modifiers, isARepeat) && shortcut.shouldTrigger() {
+3 -4
View File
@@ -229,7 +229,7 @@ class App: AppCenterApplication, NSApplicationDelegate {
func focusSelectedWindow(_ selectedWindow: Window?) {
hideUi(true)
if let window = selectedWindow, !MissionControl.isActive() {
if let window = selectedWindow, MissionControl.state() == .inactive {
window.focus()
} else {
previewPanel.orderOut(nil)
@@ -269,12 +269,11 @@ class App: AppCenterApplication, NSApplicationDelegate {
}
func showUiOrCycleSelection(_ shortcutIndex: Int) {
logger.i("showUiOrCycleSelection")
logger.d(shortcutIndex, self.shortcutIndex, isFirstSummon)
App.app.appIsBeingUsed = true
if isFirstSummon || shortcutIndex != self.shortcutIndex {
logger.i("showUiOrCycleSelection: isFirstSummon")
isFirstSummon = false
if Windows.list.count == 0 || MissionControl.isActive() { hideUi(); return }
if Windows.list.count == 0 || MissionControl.state() != .inactive { hideUi(); return }
// TODO: can the CGS call inside detectTabbedWindows introduce latency when WindowServer is busy?
Windows.detectTabbedWindows()
// TODO: find a way to update space info when spaces are changed, instead of on every trigger