fix: sometimes switching apps wouldn't be noticed

NSRunningApplication.init(processIdentifier: pid) sometimes returns nil for Chrome, even though I see that the pid exist in Activity Monitor and is actually Chrome. I think the constructor is thread-unsafe, and should be called from the main thread only, thus I refactored to make that possible
This commit is contained in:
Louis Pontoise authored and lwouis committed 2020-06-07 23:43:40 +09:00
1 parent c7e5daa357
commit 96a6ae6e07
3 files changed
+49 -44

No files matched your search

+6 -8
View File
@@ -45,23 +45,22 @@ extension AXUIElement {
return nil
}
func isActualWindow(_ bundleIdentifier: String?) throws -> Bool {
func isActualWindow(_ bundleIdentifier: String?, _ wid: CGWindowID, _ isOnNormalLevel: Bool, _ title: String?, _ subrole: String?, _ role: String?) -> Bool {
// Some non-windows have cgWindowId == 0 (e.g. windows of apps starting at login with the checkbox "Hidden" checked)
// Some non-windows have title: nil (e.g. some OS elements)
// Some non-windows have subrole: nil (e.g. some OS elements), "AXUnknown" (e.g. Bartender), "AXSystemDialog" (e.g. Intellij tooltips)
// Minimized windows or windows of a hidden app have subrole "AXDialog"
// Activity Monitor main window subrole is "AXDialog" for a brief moment at launch; it then becomes "AXStandardWindow"
// CGWindowLevel == .normalWindow helps filter out iStats Pro and other top-level pop-overs
let wid = try cgWindowId()
return try wid != nil && wid != 0 &&
// don't show floating windows
isOnNormalLevel(wid!) &&
(["AXStandardWindow", "AXDialog"].contains(subrole()) ||
isOnNormalLevel &&
(["AXStandardWindow", "AXDialog"].contains(subrole) ||
// All Steam windows have subrole == AXUnknown
// some dropdown menus are not desirable; they have title == "", or sometimes role == nil when switching between menus quickly
(bundleIdentifier == "com.valvesoftware.steam" && title() != "" && role() != nil) ||
(bundleIdentifier == "com.valvesoftware.steam" && title != "" && role != nil) ||
// Firefox fullscreen video have subrole == AXUnknown if fullscreen'ed when the base window is not fullscreen
(bundleIdentifier == "org.mozilla.firefox" && role() == "AXWindow")
(bundleIdentifier == "org.mozilla.firefox" && role == "AXWindow")
)
}
@@ -91,8 +90,7 @@ extension AXUIElement {
}
func isFullscreen() throws -> Bool {
return try
attribute(kAXFullscreenAttribute, Bool.self) == true
return try attribute(kAXFullscreenAttribute, Bool.self) == true
}
func focusedWindow() throws -> AXUIElement? {
+15 -14
View File
@@ -54,17 +54,18 @@ class Application: NSObject {
guard let self = self else { return }
if let windows_ = try self.axUiElement!.windows(), windows_.count > 0 {
// bug in macOS: sometimes the OS returns multiple duplicate windows (e.g. Mail.app starting at login)
let windows = try Array(Set(windows_)).map {
(
$0,
try $0.isActualWindow(self.runningApplication.bundleIdentifier),
try $0.cgWindowId(),
try $0.title(),
try $0.isFullscreen(),
try $0.isMinimized(),
try $0.position()
)
}
let windows = try Array(Set(windows_)).compactMap {
if let wid = try $0.cgWindowId() {
let title = try $0.title()
let subrole = try $0.subrole()
let role = try $0.role()
let isOnNormalLevel = try $0.isOnNormalLevel(wid)
if $0.isActualWindow(self.runningApplication.bundleIdentifier, wid, isOnNormalLevel, title, subrole, role) {
return ($0, wid, title, try $0.isFullscreen(), try $0.isMinimized(), try $0.position())
}
}
return nil
} as [(AXUIElement, CGWindowID, String?, Bool, Bool, CGPoint?)]
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.addWindows(windows)
@@ -74,9 +75,9 @@ class Application: NSObject {
}
}
private func addWindows(_ axWindows: [(AXUIElement, Bool, CGWindowID?, String?, Bool, Bool, CGPoint?)]) {
let windows: [Window] = axWindows.compactMap { (axUiElement, isActualWindow, wid, axTitle, isFullscreen, isMinimized, position) in
if let wid = wid, isActualWindow && Windows.list.firstIndexThatMatches(axUiElement, wid) == nil {
private func addWindows(_ axWindows: [(AXUIElement, CGWindowID, String?, Bool, Bool, CGPoint?)]) {
let windows: [Window] = axWindows.compactMap { (axUiElement, wid, axTitle, isFullscreen, isMinimized, position) in
if Windows.list.firstIndexThatMatches(axUiElement, wid) == nil {
return Window(axUiElement, self, wid, axTitle, isFullscreen, isMinimized, position)
}
return nil
+28 -22
View File
@@ -23,33 +23,32 @@ func retryUntilTimeout(_ fn: @escaping () throws -> Void, _ startTime: DispatchT
func handleEvent(_ type: String, _ element: AXUIElement) throws {
debugPrint("Accessibility event", type, element)
// events are handled concurrently, thus we check that the app is still running
if let pid = try element.pid(),
let app = NSRunningApplication(processIdentifier: pid) {
if let pid = try element.pid() {
switch type {
case kAXApplicationActivatedNotification: try applicationActivated(element)
case kAXApplicationHiddenNotification,
kAXApplicationShownNotification: try applicationHiddenOrShown(element, app, type)
case kAXWindowCreatedNotification: try windowCreated(element, app)
case kAXMainWindowChangedNotification: try focusedWindowChanged(element, app)
kAXApplicationShownNotification: try applicationHiddenOrShown(element, pid, type)
case kAXWindowCreatedNotification: try windowCreated(element, pid)
case kAXMainWindowChangedNotification: try focusedWindowChanged(element, pid)
case kAXUIElementDestroyedNotification: try windowDestroyed(element)
case kAXWindowMiniaturizedNotification,
kAXWindowDeminiaturizedNotification: try windowMiniaturizedOrDeminiaturized(element, type)
case kAXTitleChangedNotification: try windowTitleChanged(element)
case kAXWindowResizedNotification: try windowResized(element)
case kAXWindowMovedNotification: try windowMoved(element)
case kAXFocusedUIElementChangedNotification: try focusedUiElementChanged(element, app)
case kAXFocusedUIElementChangedNotification: try focusedUiElementChanged(element, pid)
default: return
}
}
}
private func focusedUiElementChanged(_ element: AXUIElement, _ app: NSRunningApplication) throws {
let appAxUiElement = AXUIElementCreateApplication(app.processIdentifier)
private func focusedUiElementChanged(_ element: AXUIElement, _ pid: pid_t) throws {
let appAxUiElement = AXUIElementCreateApplication(pid)
if let currentWindows = try appAxUiElement.windows() {
DispatchQueue.main.async {
let windows = Windows.list.filter {
// for AXUIElement of apps, CFEqual or == don't work; looks like a Cocoa bug
let isFromApp = $0.application.runningApplication.processIdentifier == app.processIdentifier
let isFromApp = $0.application.runningApplication.processIdentifier == pid
if isFromApp {
// this event is the only opportunity we have to check if a window became a tab, or a tab became a window
let isTabbedNew = $0.getIsTabbed(currentWindows)
@@ -70,17 +69,17 @@ private func applicationActivated(_ element: AXUIElement) throws {
let wid = try appFocusedWindow.cgWindowId() {
DispatchQueue.main.async {
guard let existingIndex = Windows.list.firstIndexThatMatches(appFocusedWindow, wid) else { return }
Windows.list.insertAndScaleRecycledPool(Windows.list.remove(at: existingIndex), at: 0)
Windows.list.insertAndScaleRecycledPool(Windows.list.remove(at: existingIndex), at: 0)
App.app.refreshOpenUi([Windows.list[0], Windows.list[existingIndex]])
}
}
}
private func applicationHiddenOrShown(_ element: AXUIElement, _ app: NSRunningApplication, _ type: String) throws {
private func applicationHiddenOrShown(_ element: AXUIElement, _ pid: pid_t, _ type: String) throws {
DispatchQueue.main.async {
let windows = Windows.list.filter {
// for AXUIElement of apps, CFEqual or == don't work; looks like a Cocoa bug
let isFromApp = $0.application.runningApplication.processIdentifier == app.processIdentifier
let isFromApp = $0.application.runningApplication.processIdentifier == pid
if isFromApp {
$0.isHidden = type == kAXApplicationHiddenNotification
}
@@ -90,19 +89,23 @@ private func applicationHiddenOrShown(_ element: AXUIElement, _ app: NSRunningAp
}
}
private func windowCreated(_ element: AXUIElement, _ app: NSRunningApplication) throws {
if let wid = try element.cgWindowId(),
try element.isActualWindow(app.bundleIdentifier) {
private func windowCreated(_ element: AXUIElement, _ pid: pid_t) throws {
if let wid = try element.cgWindowId() {
let axTitle = try element.title()
let subrole = try element.subrole()
let role = try element.role()
let isFullscreen = try element.isFullscreen()
let isMinimized = try element.isMinimized()
let isOnNormalLevel = try element.isOnNormalLevel(wid)
let position = try element.position()
DispatchQueue.main.async {
// a window being un-minimized can trigger kAXWindowCreatedNotification
if Windows.list.firstIndexThatMatches(element, wid) == nil,
let app = (Applications.list.first { $0.runningApplication.processIdentifier == app.processIdentifier }) {
let runningApp = NSRunningApplication(processIdentifier: pid),
element.isActualWindow(runningApp.bundleIdentifier, wid, isOnNormalLevel, axTitle, subrole, role),
let app = (Applications.list.first { $0.runningApplication.processIdentifier == pid }) {
let window = Window(element, app, wid, axTitle, isFullscreen, isMinimized, position)
Windows.list.insertAndScaleRecycledPool(window, at: 0)
Windows.list.insertAndScaleRecycledPool(window, at: 0)
Windows.cycleFocusedWindowIndex(1)
App.app.refreshOpenUi([window])
}
@@ -110,19 +113,22 @@ private func windowCreated(_ element: AXUIElement, _ app: NSRunningApplication)
}
}
private func focusedWindowChanged(_ element: AXUIElement, _ app: NSRunningApplication) throws {
private func focusedWindowChanged(_ element: AXUIElement, _ pid: pid_t) throws {
if let wid = try element.cgWindowId() {
let isActualWindow = try element.isActualWindow(app.bundleIdentifier)
let axTitle = try element.title()
let subrole = try element.subrole()
let role = try element.role()
let isFullscreen = try element.isFullscreen()
let isMinimized = try element.isMinimized()
let isOnNormalLevel = try element.isOnNormalLevel(wid)
let position = try element.position()
DispatchQueue.main.async {
if let existingIndex = Windows.list.firstIndexThatMatches(element, wid) {
Windows.list.insertAndScaleRecycledPool(Windows.list.remove(at: existingIndex), at: 0)
Windows.list.insertAndScaleRecycledPool(Windows.list.remove(at: existingIndex), at: 0)
App.app.refreshOpenUi([Windows.list[0], Windows.list[existingIndex]])
} else if isActualWindow,
let app = (Applications.list.first { $0.runningApplication.processIdentifier == app.processIdentifier }) {
} else if let runningApp = NSRunningApplication(processIdentifier: pid),
element.isActualWindow(runningApp.bundleIdentifier, wid, isOnNormalLevel, axTitle, subrole, role),
let app = (Applications.list.first { $0.runningApplication.processIdentifier == pid }) {
Windows.list.insertAndScaleRecycledPool(Window(element, app, wid, axTitle, isFullscreen, isMinimized, position), at: 0)
App.app.refreshOpenUi([Windows.list[0]])
}