fix: works without screen-recording permissions (closes #3819)
This commit is contained in:
1 parent
d430f835a4
commit
f7de2bb6d9
7 files changed
+66
-9
No files matched your search
@@ -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)!)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user