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
|
import Foundation
|
||||||
|
|
||||||
class MissionControl {
|
class MissionControl {
|
||||||
static func isActive() -> Bool {
|
private static var state_ = MissionControlState.inactive
|
||||||
// when Mission Control is active, the Dock process spawns some windows. We observe this side-effect and infer
|
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) {
|
for window in CGWindow.windows(.optionOnScreenOnly) {
|
||||||
// ownerName == "Dock" && title == nil is a sign that Mission Control may be active
|
// ownerName == "Dock" && title == nil is a sign that Mission Control may be active
|
||||||
if window.ownerName() == "Dock" && window.title() == nil
|
if window.ownerName() == "Dock" && window.title() == nil
|
||||||
// layer == 500 can be a false positive when a user drags a file from a Dock folder
|
// 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
|
// see https://github.com/lwouis/alt-tab-macos/issues/706
|
||||||
&& window.layer() != 500 {
|
&& window.layer() != 500 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
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
|
// for some reason, these attributes are missing from AXRoleConstants
|
||||||
let kAXDocumentWindowSubrole = "AXDocumentWindow"
|
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 {
|
enum CGSSymbolicHotKey: Int, CaseIterable {
|
||||||
case commandTab = 1
|
case commandTab = 1
|
||||||
case commandShiftTab = 2
|
case commandShiftTab = 2
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class ATShortcut {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func executeAction(_ isARepeat: Bool) {
|
func executeAction(_ isARepeat: Bool) {
|
||||||
|
logger.i(id)
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
KeyRepeatTimer.isARepeat = isARepeat
|
KeyRepeatTimer.isARepeat = isARepeat
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ class Applications {
|
|||||||
|
|
||||||
static func addRunningApplications(_ runningApps: [NSRunningApplication]) {
|
static func addRunningApplications(_ runningApps: [NSRunningApplication]) {
|
||||||
runningApps.forEach {
|
runningApps.forEach {
|
||||||
|
if $0.bundleIdentifier == "com.apple.dock" {
|
||||||
|
MissionControl.observe($0.processIdentifier)
|
||||||
|
}
|
||||||
if isActualApplication($0) {
|
if isActualApplication($0) {
|
||||||
Applications.list.append(Application($0))
|
Applications.list.append(Application($0))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class BackgroundWork {
|
|||||||
static var mouseEventsThread: BackgroundThreadWithRunLoop!
|
static var mouseEventsThread: BackgroundThreadWithRunLoop!
|
||||||
static var systemPermissionsThread: BackgroundThreadWithRunLoop!
|
static var systemPermissionsThread: BackgroundThreadWithRunLoop!
|
||||||
static var repeatingKeyThread: BackgroundThreadWithRunLoop!
|
static var repeatingKeyThread: BackgroundThreadWithRunLoop!
|
||||||
|
static var missionControlThread: BackgroundThreadWithRunLoop!
|
||||||
|
|
||||||
// we cap concurrent tasks to .processorCount to avoid thread explosion on the .global queue
|
// we cap concurrent tasks to .processorCount to avoid thread explosion on the .global queue
|
||||||
static let globalSemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.processorCount)
|
static let globalSemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.processorCount)
|
||||||
@@ -25,6 +26,7 @@ class BackgroundWork {
|
|||||||
accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread", .userInteractive)
|
accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread", .userInteractive)
|
||||||
mouseEventsThread = BackgroundThreadWithRunLoop("mouseEventsThread", .userInteractive)
|
mouseEventsThread = BackgroundThreadWithRunLoop("mouseEventsThread", .userInteractive)
|
||||||
repeatingKeyThread = BackgroundThreadWithRunLoop("repeatingKeyThread", .userInteractive)
|
repeatingKeyThread = BackgroundThreadWithRunLoop("repeatingKeyThread", .userInteractive)
|
||||||
|
missionControlThread = BackgroundThreadWithRunLoop("missionControlThread", .userInteractive)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func startSystemPermissionThread() {
|
static func startSystemPermissionThread() {
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ class KeyboardEvents {
|
|||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: UInt32?, _ isARepeat: Bool) -> Bool {
|
fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: UInt32?, _ isARepeat: Bool) -> Bool {
|
||||||
|
logger.d(id, shortcutState, keyCode, modifiers, isARepeat)
|
||||||
var someShortcutTriggered = false
|
var someShortcutTriggered = false
|
||||||
for shortcut in ControlsTab.shortcuts.values {
|
for shortcut in ControlsTab.shortcuts.values {
|
||||||
if shortcut.matches(id, shortcutState, keyCode, modifiers, isARepeat) && shortcut.shouldTrigger() {
|
if shortcut.matches(id, shortcutState, keyCode, modifiers, isARepeat) && shortcut.shouldTrigger() {
|
||||||
|
|||||||
+3
-4
@@ -229,7 +229,7 @@ class App: AppCenterApplication, NSApplicationDelegate {
|
|||||||
|
|
||||||
func focusSelectedWindow(_ selectedWindow: Window?) {
|
func focusSelectedWindow(_ selectedWindow: Window?) {
|
||||||
hideUi(true)
|
hideUi(true)
|
||||||
if let window = selectedWindow, !MissionControl.isActive() {
|
if let window = selectedWindow, MissionControl.state() == .inactive {
|
||||||
window.focus()
|
window.focus()
|
||||||
} else {
|
} else {
|
||||||
previewPanel.orderOut(nil)
|
previewPanel.orderOut(nil)
|
||||||
@@ -269,12 +269,11 @@ class App: AppCenterApplication, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func showUiOrCycleSelection(_ shortcutIndex: Int) {
|
func showUiOrCycleSelection(_ shortcutIndex: Int) {
|
||||||
logger.i("showUiOrCycleSelection")
|
logger.d(shortcutIndex, self.shortcutIndex, isFirstSummon)
|
||||||
App.app.appIsBeingUsed = true
|
App.app.appIsBeingUsed = true
|
||||||
if isFirstSummon || shortcutIndex != self.shortcutIndex {
|
if isFirstSummon || shortcutIndex != self.shortcutIndex {
|
||||||
logger.i("showUiOrCycleSelection: isFirstSummon")
|
|
||||||
isFirstSummon = false
|
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?
|
// TODO: can the CGS call inside detectTabbedWindows introduce latency when WindowServer is busy?
|
||||||
Windows.detectTabbedWindows()
|
Windows.detectTabbedWindows()
|
||||||
// TODO: find a way to update space info when spaces are changed, instead of on every trigger
|
// TODO: find a way to update space info when spaces are changed, instead of on every trigger
|
||||||
|
|||||||
Reference in new issue
Block a user