perf: throttle all window/app ax calls
This commit is contained in:
1 parent
4f5e32593b
commit
337e2fbfec
5 files changed
+89
-89
No files matched your search
@@ -15,8 +15,12 @@ extension AXUIElement {
|
|||||||
private static let axCallsRetriesQueueTimeoutInSeconds = Float(6)
|
private static let axCallsRetriesQueueTimeoutInSeconds = Float(6)
|
||||||
// once an app is unresponsive, let's ignore other AX calls for it to avoid congestion
|
// once an app is unresponsive, let's ignore other AX calls for it to avoid congestion
|
||||||
private static var axCallsRetriesQueueUnresponsiveAppsMap = ConcurrentMap<String, UInt64>()
|
private static var axCallsRetriesQueueUnresponsiveAppsMap = ConcurrentMap<String, UInt64>()
|
||||||
// some events like window resizing trigger in quick succession. We debounce those
|
private static var throttleMap = ConcurrentMap<String, ThrottleState>()
|
||||||
private static var eventsDebounceMap = ConcurrentMap<String, DispatchWorkItem>()
|
|
||||||
|
private struct ThrottleState {
|
||||||
|
let time: UInt64
|
||||||
|
var tailScheduled: Bool
|
||||||
|
}
|
||||||
|
|
||||||
static func setGlobalTimeout() {
|
static func setGlobalTimeout() {
|
||||||
AXUIElementSetMessagingTimeout(AXUIElementCreateSystemWide(), globalMessagingTimeoutInSeconds)
|
AXUIElementSetMessagingTimeout(AXUIElementCreateSystemWide(), globalMessagingTimeoutInSeconds)
|
||||||
@@ -31,73 +35,70 @@ extension AXUIElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// if the window server is busy, it may not reply to AX calls. We retry right before the call times-out and returns a bogus value
|
/// if the window server is busy, it may not reply to AX calls. We retry right before the call times-out and returns a bogus value
|
||||||
static func retryAxCallUntilTimeout(file: String = #file, function: String = #function, line: Int = #line, context: String = "", after: DispatchTime? = nil, debounceType: DebounceType? = nil, pid: pid_t? = nil, wid: CGWindowID? = nil, retriesQueue: Bool = false, startTimeInNanoseconds: UInt64 = DispatchTime.now().uptimeNanoseconds, callType: AXCallType, block: @escaping () throws -> Void) {
|
static func retryAxCallUntilTimeout(file: String = #file, function: String = #function, line: Int = #line, context: String = "", after: DispatchTime? = nil, pid: pid_t? = nil, wid: CGWindowID? = nil, retriesQueue: Bool = false, startTimeInNanoseconds: UInt64 = DispatchTime.now().uptimeNanoseconds, callType: AXCallType, block: @escaping () throws -> Void) {
|
||||||
let closure = { retryAxCallUntilTimeout_(file: file, function: function, line: line, context: context, after: after, debounceType: debounceType, pid: pid, wid: wid, retriesQueue: retriesQueue, startTimeInNanoseconds: startTimeInNanoseconds, callType: callType, block: block) }
|
let closure = { retryAxCallUntilTimeout_(file: file, function: function, line: line, context: context, after: after, pid: pid, wid: wid, retriesQueue: retriesQueue, startTimeInNanoseconds: startTimeInNanoseconds, callType: callType, block: block) }
|
||||||
let queue = callType == .updateAppWindows ? BackgroundWork.axCallsManualDiscoveryQueue : (retriesQueue ? BackgroundWork.axCallsRetriesQueue : BackgroundWork.axCallsFirstAttemptQueue)
|
let queue = (callType == .updateAppWindows ? BackgroundWork.axCallsManualDiscoveryQueue
|
||||||
|
: (retriesQueue ? BackgroundWork.axCallsRetriesQueue
|
||||||
|
: BackgroundWork.axCallsFirstAttemptQueue))!
|
||||||
|
if callType == .updateWindowFromAxEvent || callType == .updateWindowFromManualDiscovery {
|
||||||
|
guard let wid else { Logger.error { (callType, context, file, function, line) }; return }
|
||||||
|
if throttleOrProceed(key: "\(callType.rawValue)-wid-\(wid)", queue: queue, closure: closure) { return }
|
||||||
|
}
|
||||||
|
if callType == .updateApp || callType == .updateAppWindows || callType == .updateAppFocusedWindow {
|
||||||
|
guard let pid else { Logger.error { (callType, context, file, function, line) }; return }
|
||||||
|
if throttleOrProceed(key: "\(callType.rawValue)-pid-\(pid)", queue: queue, closure: closure) { return }
|
||||||
|
}
|
||||||
if let after {
|
if let after {
|
||||||
queue!.addOperationAfter(deadline: after, block: closure)
|
queue.addOperationAfter(deadline: after, block: closure)
|
||||||
} else if let debounceType, let wid {
|
|
||||||
let debounceMapKey = "\(debounceType.rawValue)\(wid)"
|
|
||||||
let workItem = DispatchWorkItem {
|
|
||||||
closure()
|
|
||||||
eventsDebounceMap.withLock { $0[debounceMapKey] = nil }
|
|
||||||
}
|
|
||||||
eventsDebounceMap.withLock {
|
|
||||||
$0[debounceMapKey]?.cancel()
|
|
||||||
$0[debounceMapKey] = workItem
|
|
||||||
}
|
|
||||||
queue!.addOperationAfter(deadline: .now() + humanPerceptionDelay) {
|
|
||||||
workItem.perform()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
queue!.addOperation(closure)
|
queue.addOperation(closure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func retryAxCallUntilTimeout_(file: String, function: String, line: Int, context: String, after: DispatchTime?, debounceType: DebounceType?, pid: pid_t?, wid: CGWindowID?, retriesQueue: Bool, startTimeInNanoseconds: UInt64, callType: AXCallType, block: @escaping () throws -> Void) {
|
private static func retryAxCallUntilTimeout_(file: String, function: String, line: Int, context: String, after: DispatchTime?, pid: pid_t?, wid: CGWindowID?, retriesQueue: Bool, startTimeInNanoseconds: UInt64, callType: AXCallType, block: @escaping () throws -> Void) {
|
||||||
// attempt the AX call
|
// attempt the AX call
|
||||||
if (try? block()) != nil {
|
if (try? block()) != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// do we already have ongoing retries for this pid? The app is likely unresponsive
|
// give up?
|
||||||
// if their are common updates, we avoid congestion by only retrying the latest update
|
|
||||||
if let pid, callType == .updateWindow || callType == .updateAppWindows {
|
|
||||||
let unresponsiveAppsMapKey = "\(callType.rawValue)\(pid)"
|
|
||||||
let time = axCallsRetriesQueueUnresponsiveAppsMap.withLock { $0[unresponsiveAppsMapKey] }
|
|
||||||
if let time {
|
|
||||||
if startTimeInNanoseconds > time {
|
|
||||||
// new most recent call; replace and retry
|
|
||||||
axCallsRetriesQueueUnresponsiveAppsMap.withLock { $0[unresponsiveAppsMapKey] = startTimeInNanoseconds }
|
|
||||||
} else if startTimeInNanoseconds == time {
|
|
||||||
// most recent call; retry
|
|
||||||
} else {
|
|
||||||
// old call which has been replaced; ignore
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// first call; set and retry
|
|
||||||
axCallsRetriesQueueUnresponsiveAppsMap.withLock { $0[unresponsiveAppsMapKey] = startTimeInNanoseconds }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// should we give up?
|
|
||||||
let timePassedInSeconds = Float(DispatchTime.now().uptimeNanoseconds - startTimeInNanoseconds) / 1_000_000_000
|
let timePassedInSeconds = Float(DispatchTime.now().uptimeNanoseconds - startTimeInNanoseconds) / 1_000_000_000
|
||||||
if timePassedInSeconds >= axCallsRetriesQueueTimeoutInSeconds {
|
if timePassedInSeconds >= axCallsRetriesQueueTimeoutInSeconds {
|
||||||
Logger.info { "AX call failed for more than \(Int(axCallsRetriesQueueTimeoutInSeconds))s. Giving up on it. \(logFromContext(file, function, line, context, callType))" }
|
Logger.info { "AX call failed for more than \(Int(axCallsRetriesQueueTimeoutInSeconds))s. Giving up on it. \(logFromContext(file, function, line, context, callType))" }
|
||||||
if let pid, callType == .updateWindow || callType == .updateAppWindows {
|
|
||||||
let unresponsiveAppsMapKey = "\(callType.rawValue)\(pid)"
|
|
||||||
axCallsRetriesQueueUnresponsiveAppsMap.withLock { $0.removeValue(forKey: unresponsiveAppsMapKey) }
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// retry
|
// retry
|
||||||
Logger.debug { "(pid:\(pid) wid:\(wid)) \(logFromContext(file, function, line, context, callType))" }
|
Logger.debug { "(pid:\(pid) wid:\(wid)) \(logFromContext(file, function, line, context, callType))" }
|
||||||
retryAxCallUntilTimeout(file: file, function: function, line: line, context: context, after: .now() + humanPerceptionDelay, debounceType: debounceType, pid: pid, wid: wid, retriesQueue: true, startTimeInNanoseconds: startTimeInNanoseconds, callType: callType, block: block)
|
retryAxCallUntilTimeout(file: file, function: function, line: line, context: context, after: .now() + humanPerceptionDelay, pid: pid, wid: wid, retriesQueue: true, startTimeInNanoseconds: startTimeInNanoseconds, callType: callType, block: block)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func logFromContext(_ file: String, _ function: String, _ line: Int, _ context: String, _ callType: AXCallType) -> String {
|
private static func logFromContext(_ file: String, _ function: String, _ line: Int, _ context: String, _ callType: AXCallType) -> String {
|
||||||
return "Context: \((file as NSString).lastPathComponent):\(line) \(function) \(String(describing: callType)) \(context)"
|
return "Context: \((file as NSString).lastPathComponent):\(line) \(function) \(String(describing: callType)) \(context)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func throttleOrProceed(key: String, queue: LabeledOperationQueue, closure: @escaping () -> Void) -> Bool {
|
||||||
|
let windowNs: UInt64 = 1_000_000_000
|
||||||
|
return throttleMap.withLock { map in
|
||||||
|
let now = DispatchTime.now().uptimeNanoseconds
|
||||||
|
if let state = map[key] {
|
||||||
|
let elapsed = now >= state.time ? (now - state.time) : windowNs // treat “time in future” as throttled
|
||||||
|
if elapsed < windowNs {
|
||||||
|
if !state.tailScheduled {
|
||||||
|
map[key] = ThrottleState(time: state.time, tailScheduled: true)
|
||||||
|
|
||||||
|
let remaining = windowNs - elapsed
|
||||||
|
queue.addOperationAfter(deadline: .now() + .nanoseconds(Int(remaining))) {
|
||||||
|
throttleMap.withLock { $0[key] = nil }
|
||||||
|
closure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map[key] = ThrottleState(time: now, tailScheduled: false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func subscribeToNotification(_ axObserver: AXObserver, _ notification: String, _ callback: (() -> Void)? = nil) throws -> Bool {
|
func subscribeToNotification(_ axObserver: AXObserver, _ notification: String, _ callback: (() -> Void)? = nil) throws -> Bool {
|
||||||
let result = AXObserverAddNotification(axObserver, self, notification as CFString, nil)
|
let result = AXObserverAddNotification(axObserver, self, notification as CFString, nil)
|
||||||
@@ -116,7 +117,10 @@ extension AXUIElement {
|
|||||||
case subscribeToAppNotification
|
case subscribeToAppNotification
|
||||||
case subscribeToWindowNotification
|
case subscribeToWindowNotification
|
||||||
case subscribeToDockNotification
|
case subscribeToDockNotification
|
||||||
case updateWindow
|
case updateWindowFromAxEvent
|
||||||
|
case updateWindowFromManualDiscovery
|
||||||
|
case updateApp
|
||||||
|
case updateAppFocusedWindow
|
||||||
case updateAppWindows
|
case updateAppWindows
|
||||||
case updateDockBadges
|
case updateDockBadges
|
||||||
case axEventEntrypoint
|
case axEventEntrypoint
|
||||||
@@ -316,8 +320,3 @@ struct AXAttributes {
|
|||||||
var position: CGPoint?
|
var position: CGPoint?
|
||||||
var size: CGSize?
|
var size: CGSize?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DebounceType: Int {
|
|
||||||
case windowResizedOrMoved
|
|
||||||
case windowTitleChanged
|
|
||||||
}
|
|
||||||
+18
-12
@@ -167,23 +167,29 @@ class Application: NSObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for axWindow in axWindows {
|
for axWindow in axWindows {
|
||||||
let wid = try axWindow.cgWindowId()
|
guard let wid = try? axWindow.cgWindowId() else { continue }
|
||||||
guard wid != 0 && wid != App.app.thumbnailsPanel.windowNumber else { continue } // some bogus "windows" have wid 0
|
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid, wid: wid, callType: .updateWindowFromManualDiscovery) { [weak self] in
|
||||||
let level = wid.level()
|
try self?.manuallyUpdateWindow(axWindow, wid)
|
||||||
let a = try axWindow.attributes([kAXTitleAttribute, kAXSubroleAttribute, kAXRoleAttribute, kAXSizeAttribute, kAXPositionAttribute, kAXFullscreenAttribute, kAXMinimizedAttribute])
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
guard let self else { return }
|
|
||||||
let findOrCreate = Windows.findOrCreate(axWindow, wid, self, level, a.title, a.subrole, a.role, a.size, a.position, a.isFullscreen, a.isMinimized)
|
|
||||||
guard let window = findOrCreate.0 else { return }
|
|
||||||
if findOrCreate.1 {
|
|
||||||
Logger.info { "manuallyUpdateWindows found a new window:\(window.debugId)" }
|
|
||||||
App.app.refreshOpenUiAfterExternalEvent([window])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func manuallyUpdateWindow(_ axWindow: AXUIElement, _ wid: CGWindowID) throws {
|
||||||
|
guard wid != 0 && wid != App.app.thumbnailsPanel.windowNumber else { return } // some bogus "windows" have wid 0
|
||||||
|
let level = wid.level()
|
||||||
|
let a = try axWindow.attributes([kAXTitleAttribute, kAXSubroleAttribute, kAXRoleAttribute, kAXSizeAttribute, kAXPositionAttribute, kAXFullscreenAttribute, kAXMinimizedAttribute])
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
let findOrCreate = Windows.findOrCreate(axWindow, wid, self, level, a.title, a.subrole, a.role, a.size, a.position, a.isFullscreen, a.isMinimized)
|
||||||
|
guard let window = findOrCreate.0 else { return }
|
||||||
|
if findOrCreate.1 {
|
||||||
|
Logger.info { "manuallyUpdateWindows found a new window:\(window.debugId)" }
|
||||||
|
App.app.refreshOpenUiAfterExternalEvent([window])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func addWindowlessWindowIfNeeded() -> Window? {
|
func addWindowlessWindowIfNeeded() -> Window? {
|
||||||
guard runningApplication.activationPolicy == .regular && !runningApplication.isTerminated
|
guard runningApplication.activationPolicy == .regular && !runningApplication.isTerminated
|
||||||
|
|||||||
@@ -87,12 +87,12 @@ class Window {
|
|||||||
private func observeEvents() {
|
private func observeEvents() {
|
||||||
AXObserverCreate(application.pid, AccessibilityEvents.axObserverCallback, &axObserver)
|
AXObserverCreate(application.pid, AccessibilityEvents.axObserverCallback, &axObserver)
|
||||||
guard let axObserver else { return }
|
guard let axObserver else { return }
|
||||||
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, callType: .subscribeToWindowNotification) { [weak self] in
|
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, wid: cgWindowId, callType: .subscribeToWindowNotification) { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
if try self.axUiElement!.subscribeToNotification(axObserver, Window.notifications.first!) {
|
if try self.axUiElement!.subscribeToNotification(axObserver, Window.notifications.first!) {
|
||||||
Logger.debug { "Subscribed to window: \(self.debugId)" }
|
Logger.debug { "Subscribed to window: \(self.debugId)" }
|
||||||
for notification in Window.notifications.dropFirst() {
|
for notification in Window.notifications.dropFirst() {
|
||||||
AXUIElement.retryAxCallUntilTimeout(context: self.debugId, pid: self.application.pid, callType: .subscribeToWindowNotification) { [weak self] in
|
AXUIElement.retryAxCallUntilTimeout(context: self.debugId, pid: self.application.pid, wid: cgWindowId, callType: .subscribeToWindowNotification) { [weak self] in
|
||||||
try self?.axUiElement!.subscribeToNotification(axObserver, notification)
|
try self?.axUiElement!.subscribeToNotification(axObserver, notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,7 +315,7 @@ class Window {
|
|||||||
/// some apps will not trigger AXApplicationActivated, where we usually update application.focusedWindow
|
/// some apps will not trigger AXApplicationActivated, where we usually update application.focusedWindow
|
||||||
/// workaround: we check and possibly do it here
|
/// workaround: we check and possibly do it here
|
||||||
private func checkIfFocused() {
|
private func checkIfFocused() {
|
||||||
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, callType: .updateWindow) { [weak self] in
|
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, wid: cgWindowId, callType: .updateAppFocusedWindow) { [weak self] in
|
||||||
guard let self,
|
guard let self,
|
||||||
let focusedWindow = try self.application.axUiElement?.attributes([kAXFocusedWindowAttribute]).focusedWindow,
|
let focusedWindow = try self.application.axUiElement?.attributes([kAXFocusedWindowAttribute]).focusedWindow,
|
||||||
let window = try (Windows.list.first { $0.isEqualRobust(focusedWindow, (try focusedWindow.cgWindowId())) }) else { return }
|
let window = try (Windows.list.first { $0.isEqualRobust(focusedWindow, (try focusedWindow.cgWindowId())) }) else { return }
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class Windows {
|
|||||||
static func updateIsFullscreenOnCurrentSpace() {
|
static func updateIsFullscreenOnCurrentSpace() {
|
||||||
let windowsOnCurrentSpace = list.filter { !$0.isWindowlessApp }
|
let windowsOnCurrentSpace = list.filter { !$0.isWindowlessApp }
|
||||||
for window in windowsOnCurrentSpace {
|
for window in windowsOnCurrentSpace {
|
||||||
AXUIElement.retryAxCallUntilTimeout(context: window.debugId, after: .now() + humanPerceptionDelay, callType: .updateWindow) { [weak window] in
|
AXUIElement.retryAxCallUntilTimeout(context: window.debugId, after: .now() + humanPerceptionDelay, wid: window.cgWindowId, callType: .updateWindowFromAxEvent) { [weak window] in
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
// we reuse existing code, to update .isFullscreen, as if there was a kAXWindowResizedNotification
|
// we reuse existing code, to update .isFullscreen, as if there was a kAXWindowResizedNotification
|
||||||
try AccessibilityEvents.handleEventWindow(kAXWindowResizedNotification, window.cgWindowId!, window.application.pid, window.axUiElement!)
|
try AccessibilityEvents.handleEventWindow(kAXWindowResizedNotification, window.cgWindowId!, window.application.pid, window.axUiElement!)
|
||||||
|
|||||||
@@ -15,34 +15,40 @@ class AccessibilityEvents {
|
|||||||
let pid = try element.pid()
|
let pid = try element.pid()
|
||||||
Logger.debug { "\(type) pid:\(pid)" }
|
Logger.debug { "\(type) pid:\(pid)" }
|
||||||
if [kAXApplicationActivatedNotification, kAXApplicationHiddenNotification, kAXApplicationShownNotification].contains(type) {
|
if [kAXApplicationActivatedNotification, kAXApplicationHiddenNotification, kAXApplicationShownNotification].contains(type) {
|
||||||
try handleEventApp(type, pid, element)
|
AXUIElement.retryAxCallUntilTimeout(context: "(pid:\(pid))", pid: pid, callType: .updateApp) {
|
||||||
|
try handleEventApp(type, pid, element)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
try handleEventWindowWithDebounceIfNecessary(type, pid, element)
|
let wid = try element.cgWindowId()
|
||||||
|
AXUIElement.retryAxCallUntilTimeout(context: "(pid:\(pid))", pid: pid, wid: wid, callType: .updateWindowFromAxEvent) {
|
||||||
|
try handleEventWindow(type, wid, pid, element)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func handleEventApp(_ type: String, _ pid: pid_t, _ element: AXUIElement) throws {
|
private static func handleEventApp(_ type: String, _ pid: pid_t, _ element: AXUIElement) throws {
|
||||||
let appFocusedWindow = try element.attributes([kAXFocusedWindowAttribute]).focusedWindow
|
let appFocusedWindow = try element.attributes([kAXFocusedWindowAttribute]).focusedWindow
|
||||||
|
let wid = try appFocusedWindow?.cgWindowId()
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
guard let app = Applications.findOrCreate(pid) else { return }
|
guard let app = Applications.findOrCreate(pid) else { return }
|
||||||
Logger.info { "\(type) app:\(app.debugId)" }
|
Logger.info { "\(type) app:\(app.debugId)" }
|
||||||
if type == kAXApplicationActivatedNotification {
|
if type == kAXApplicationActivatedNotification {
|
||||||
applicationActivated(app, pid, type, appFocusedWindow)
|
applicationActivated(app, pid, type, appFocusedWindow, wid)
|
||||||
} else if type == kAXApplicationHiddenNotification || type == kAXApplicationShownNotification {
|
} else if type == kAXApplicationHiddenNotification || type == kAXApplicationShownNotification {
|
||||||
applicationHiddenOrShown(app, pid, type)
|
applicationHiddenOrShown(app, pid, type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func applicationActivated(_ app: Application, _ pid: pid_t, _ type: String, _ appFocusedWindow: AXUIElement?) {
|
private static func applicationActivated(_ app: Application, _ pid: pid_t, _ type: String, _ appFocusedWindow: AXUIElement?, _ wid: CGWindowID?) {
|
||||||
Applications.frontmostPid = pid
|
Applications.frontmostPid = pid
|
||||||
if app.hasBeenActiveOnce != true {
|
if app.hasBeenActiveOnce != true {
|
||||||
app.hasBeenActiveOnce = true
|
app.hasBeenActiveOnce = true
|
||||||
}
|
}
|
||||||
if let appFocusedWindow {
|
if let appFocusedWindow, let wid {
|
||||||
// if there is a focusedWindow, we reuse existing code to process it as if it was a kAXFocusedWindowChangedNotification
|
// if there is a focusedWindow, we reuse existing code to process it as if it was a kAXFocusedWindowChangedNotification
|
||||||
AXUIElement.retryAxCallUntilTimeout(context: "(type:\(type)", callType: .axEventEntrypoint) {
|
AXUIElement.retryAxCallUntilTimeout(context: "\(type) \(app.debugId))", pid: pid, wid: wid, callType: .updateWindowFromAxEvent) {
|
||||||
try handleEventWindowWithDebounceIfNecessary(kAXFocusedWindowChangedNotification, pid, appFocusedWindow)
|
try handleEventWindow(kAXFocusedWindowChangedNotification, wid, pid, appFocusedWindow)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
App.app.checkIfShortcutsShouldBeDisabled(nil, app)
|
App.app.checkIfShortcutsShouldBeDisabled(nil, app)
|
||||||
@@ -67,20 +73,9 @@ class AccessibilityEvents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func handleEventWindowWithDebounceIfNecessary(_ type: String, _ pid: pid_t, _ element: AXUIElement) throws {
|
|
||||||
let wid = try element.cgWindowId()
|
|
||||||
guard wid != 0 || type == kAXUIElementDestroyedNotification else { return } // some bogus "windows" have wid 0
|
|
||||||
if type == kAXTitleChangedNotification || type == kAXWindowResizedNotification || type == kAXWindowMovedNotification {
|
|
||||||
AXUIElement.retryAxCallUntilTimeout(context: "(wid:\(wid) pid:\(pid))", debounceType: (type == kAXTitleChangedNotification ? .windowTitleChanged : .windowResizedOrMoved), pid: pid, wid: wid, callType: .updateWindow) {
|
|
||||||
try handleEventWindow(type, wid, pid, element)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try handleEventWindow(type, wid, pid, element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func handleEventWindow(_ type: String, _ wid: CGWindowID, _ pid: pid_t, _ element: AXUIElement) throws {
|
static func handleEventWindow(_ type: String, _ wid: CGWindowID, _ pid: pid_t, _ element: AXUIElement) throws {
|
||||||
guard wid != App.app.thumbnailsPanel.windowNumber else { return } // don't process events for the thumbnails panel
|
guard wid != 0 || type == kAXUIElementDestroyedNotification,
|
||||||
|
wid != App.app.thumbnailsPanel.windowNumber else { return } // don't process events for the thumbnails panel
|
||||||
let level = wid.level()
|
let level = wid.level()
|
||||||
let a = try element.attributes([kAXTitleAttribute, kAXSubroleAttribute, kAXRoleAttribute, kAXSizeAttribute, kAXPositionAttribute, kAXFullscreenAttribute, kAXMinimizedAttribute])
|
let a = try element.attributes([kAXTitleAttribute, kAXSubroleAttribute, kAXRoleAttribute, kAXSizeAttribute, kAXPositionAttribute, kAXFullscreenAttribute, kAXMinimizedAttribute])
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|||||||
Reference in new issue
Block a user