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:
1 parent
c7e5daa357
commit
96a6ae6e07
3 files changed
+49
-44
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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]])
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user