diff --git a/src/api-wrappers/HelperExtensions.swift b/src/api-wrappers/HelperExtensions.swift
index 80230cdb..972c6a2b 100644
--- a/src/api-wrappers/HelperExtensions.swift
+++ b/src/api-wrappers/HelperExtensions.swift
@@ -211,3 +211,18 @@ extension String {
self = NSFileTypeForHFSTypeCode(fourCharCode).trimmingCharacters(in: CharacterSet(charactersIn: "'"))
}
}
+
+fileprivate var notificationKey: Int = 1
+
+extension NSRunningApplication {
+ var notification: Notification.Name? {
+ get {
+ return objc_getAssociatedObject(self, ¬ificationKey) as? Notification.Name
+ }
+ set {
+ if let value = newValue {
+ objc_setAssociatedObject(self, ¬ificationKey, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+ }
+ }
+ }
+}
diff --git a/src/logic/Application.swift b/src/logic/Application.swift
index a8806b52..85fffd3f 100644
--- a/src/logic/Application.swift
+++ b/src/logic/Application.swift
@@ -84,8 +84,8 @@ class Application: NSObject {
func observeNewWindows(_ group: DispatchGroup? = nil) {
if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited {
retryAxCallUntilTimeout(group, 5) { [weak self] in
- guard let self = self else { return }
- if let axWindows_ = try self.axUiElement!.windows(), axWindows_.count > 0 {
+ guard let self = self, let axWindows_ = try self.axUiElement!.windows() else { throw AxError.runtimeError }
+ if axWindows_.count > 0 {
// bug in macOS: sometimes the OS returns multiple duplicate windows (e.g. Mail.app starting at login)
let axWindows = try Array(Set(axWindows_)).compactMap {
if let wid = try $0.cgWindowId() {
@@ -114,14 +114,10 @@ class Application: NSObject {
let window = self.addWindowslessAppsIfNeeded()
App.app.refreshOpenUi(window)
}
- if group == nil && !self.wasLaunchedBeforeAltTab && (
- // workaround: opening an app while the active app is fullscreen; we wait out the space transition animation
- CGSSpaceGetType(cgsMainConnectionId, Spaces.currentSpaceId) == .fullscreen ||
- // workaround: some apps launch but have no window ready instantly. It's very unlikely an app would launch with no window
- // so we retry until timeout, in those rare cases (e.g. Bear.app)
- // we only do this for active app, to avoid wasting CPU, with the trade-off of maybe missing some windows
- self.runningApplication.isActive
- ) {
+ // workaround: some apps launch but have no window ready instantly. It's very unlikely an app would launch with no window
+ // so we retry until timeout, in those rare cases (e.g. Bear.app)
+ // we only do this for active app, to avoid wasting CPU, with the trade-off of maybe missing some windows
+ if group == nil && self.runningApplication.notification == NSWorkspace.didActivateApplicationNotification {
throw AxError.runtimeError
}
}
diff --git a/src/logic/Applications.swift b/src/logic/Applications.swift
index a11ba62c..5d1cf1f7 100644
--- a/src/logic/Applications.swift
+++ b/src/logic/Applications.swift
@@ -42,7 +42,7 @@ class Applications {
static func addRunningApplications(_ runningApps: [NSRunningApplication], _ wasLaunchedBeforeAltTab: Bool = false) {
runningApps.forEach {
- if isActualApplication($0) {
+ if isActualApplication($0, wasLaunchedBeforeAltTab) {
Applications.list.append(Application($0, wasLaunchedBeforeAltTab))
}
}
@@ -102,32 +102,30 @@ class Applications {
}
}
- private static func isActualApplication(_ app: NSRunningApplication) -> Bool {
+ private static func isActualApplication(_ app: NSRunningApplication, _ wasLaunchedBeforeAltTab: Bool = false) -> Bool {
// an app can start with .activationPolicy == .prohibited, then transition to != .prohibited later
// an app can be both activationPolicy == .accessory and XPC (e.g. com.apple.dock.etci)
- return (isNotXpc(app) || isAndroidEmulator(app)) && !app.processIdentifier.isZombie() && isAnWindowApplication(app)
+ return isAnWindowApplication(app, wasLaunchedBeforeAltTab) && (isNotXpc(app) || isAndroidEmulator(app)) && !app.processIdentifier.isZombie()
}
- private static func isAnWindowApplication(_ app: NSRunningApplication) -> Bool {
- if (app.isActive) {
+ private static func isAnWindowApplication(_ app: NSRunningApplication, _ wasLaunchedBeforeAltTab: Bool = false) -> Bool {
+ if (wasLaunchedBeforeAltTab) {
+ // For wasLaunchedBeforeAltTab=true, we assume that those apps are all launched, if they are programs with windows.
+ // Even if it has 0 windows at this point, axUiElement.windows() will not throw an exception. If they are programs without windows, then axUiElement.windows() will throw an exception.
+ // Here I consider there is an edge case where AltTab is starting up and this program has been loading, then it is possible that axUiElement.windows() will throw an exception.
+ // I'm not quite sure if this happens, but even if it does, then after restarting this application, AltTab captures its window without any problem. I think this happens rarely.
+ let allWindows = CGWindow.windows(.optionAll)
+ guard let winApp = (allWindows.first { app.processIdentifier == $0.ownerPID() && $0.isNotMenubarOrOthers() && $0.id() != nil && $0.title() != nil}) else {
+ return false
+ }
+ return true
+ } else {
// Because we only add the application when we receive the didActivateApplicationNotification.
// So here is actually the handling for the case wasLaunchedBeforeAltTab=false. For applications where wasLaunchedBeforeAltTab=true, the majority of isActive is false.
// The reason for not using axUiElement.windows() here as a way to determine if it is a window application is that
// When we receive the didActivateApplicationNotification notification, the application may still be loading and axUiElement.windows() will throw an exception
// So we use isActive to determine if it is a window application, even if the application is not frontmost, isActive is still true at this time
return true;
- } else {
- do {
- // For wasLaunchedBeforeAltTab=true, we assume that those apps are all launched, if they are programs with windows.
- // Even if it has 0 windows at this point, axUiElement.windows() will not throw an exception. If they are programs without windows, then axUiElement.windows() will throw an exception.
- // Here I consider there is an edge case where AltTab is starting up and this program has been loading, then it is possible that axUiElement.windows() will throw an exception.
- // I'm not quite sure if this happens, but even if it does, then after restarting this application, AltTab captures its window without any problem. I think this happens rarely.
- let axUiElement = AXUIElementCreateApplication(app.processIdentifier)
- try axUiElement.windows()
- return true
- } catch {
- return false
- }
}
}
diff --git a/src/logic/events/AccessibilityEvents.swift b/src/logic/events/AccessibilityEvents.swift
index 0ca987ce..4258f89c 100644
--- a/src/logic/events/AccessibilityEvents.swift
+++ b/src/logic/events/AccessibilityEvents.swift
@@ -47,6 +47,12 @@ fileprivate func applicationActivated(_ element: AXUIElement, _ pid: pid_t) thro
app.focusedWindow = window
App.app.checkIfShortcutsShouldBeDisabled(window, app.runningApplication)
App.app.refreshOpenUi(window != nil ? [window!] : nil)
+ guard let win = (Windows.list.first { app.runningApplication.processIdentifier == $0.application.runningApplication.processIdentifier && !$0.isWindowlessApp }) else {
+ // for edge-case: some app (e.g. Bear.app) is loading and runningApplication.isFinishedLaunching is false (sometimes) when we call observeNewWindows() at first time
+ // as a result we miss their windows. but we will receive kAXApplicationActivatedNotification notification and we can add it successfully
+ app.observeNewWindows()
+ return
+ }
}
}
}
diff --git a/src/logic/events/WorkspaceEvents.swift b/src/logic/events/WorkspaceEvents.swift
index c2bfc012..574a9370 100644
--- a/src/logic/events/WorkspaceEvents.swift
+++ b/src/logic/events/WorkspaceEvents.swift
@@ -10,19 +10,12 @@ class WorkspaceEvents {
}
static func observerCallback(_ application: NSWorkspace, _ change: NSKeyValueObservedChange) {
- let workspaceApps = Set(NSWorkspace.shared.runningApplications)
- let diff = Array(workspaceApps.symmetricDifference(previousValueOfRunningApps))
- if change.kind == .insertion {
- debugPrint("OS event", "apps launched", diff.map { ($0.processIdentifier, $0.bundleIdentifier) })
- } else if change.kind == .removal {
- debugPrint("OS event", "apps quit", diff.map { ($0.processIdentifier, $0.bundleIdentifier) })
- Applications.removeRunningApplications(diff)
- previousValueOfRunningApps = workspaceApps
- }
}
static func registerFrontAppChangeNote() {
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(receiveFrontAppChangeNote(_:)), name: NSWorkspace.didActivateApplicationNotification, object: nil)
+ NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(receiveFrontAppLaunchNote(_:)), name: NSWorkspace.didLaunchApplicationNotification, object: nil)
+ NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(receiveFrontAppTerminateNote(_:)), name: NSWorkspace.didTerminateApplicationNotification, object: nil)
}
// We add apps when we receive a didActivateApplicationNotification notification, not when we receive an apps launched, because any app will have an apps launched notification.
@@ -32,12 +25,42 @@ class WorkspaceEvents {
// If we go to add the application when we receive the message of apps launched, at this time NSRunningApplication.isActive may be false, and try axUiElement.windows() may also throw an exception.
// For those background applications, we don't receive notifications of didActivateApplicationNotification until they have their own window. For example, those menu bar applications.
@objc static func receiveFrontAppChangeNote(_ notification: Notification) {
- if let application = notification.userInfo?["NSWorkspaceApplicationKey"] as? NSRunningApplication {
- debugPrint("OS event", "didActivateApplicationNotification", application.bundleIdentifier)
- let workspaceApps = Set(NSWorkspace.shared.runningApplications)
- let diff = Array(workspaceApps.symmetricDifference(previousValueOfRunningApps))
- Applications.addRunningApplications(diff)
- previousValueOfRunningApps = workspaceApps
+ if let application = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication {
+ debugPrint("OS event", notification.name.rawValue, application.bundleIdentifier, application.processIdentifier)
+ guard let app = (Applications.list.first { application.processIdentifier == $0.pid }) else {
+ debugPrint("add running application", application.bundleIdentifier, application.processIdentifier)
+ application.notification = notification.name
+ Applications.addRunningApplications([application])
+ return
+ }
+ guard let win = (Windows.list.first { application.processIdentifier == $0.application.runningApplication.processIdentifier && !$0.isWindowlessApp }) else {
+ app.hasBeenActiveOnce = true
+ app.runningApplication.notification = notification.name
+ app.observeNewWindows()
+ return
+ }
+ }
+ }
+
+ @objc static func receiveFrontAppLaunchNote(_ notification: Notification) {
+ if let application = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication {
+ debugPrint("OS event", notification.name.rawValue, application.bundleIdentifier, application.processIdentifier)
+ guard let app = (Applications.list.first { application.processIdentifier == $0.pid }) else {
+ debugPrint("add running application", notification.name.rawValue, application.bundleIdentifier, application.processIdentifier)
+ application.notification = notification.name
+ Applications.addRunningApplications([application])
+ return
+ }
+ }
+ }
+
+ @objc static func receiveFrontAppTerminateNote(_ notification: Notification) {
+ if let application = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication {
+ debugPrint("OS event", notification.name.rawValue, application.bundleIdentifier, application.processIdentifier)
+ if let app = (Applications.list.first { application.processIdentifier == $0.pid }) {
+ debugPrint("remove running application", application.bundleIdentifier, application.processIdentifier)
+ Applications.removeRunningApplications([application])
+ }
}
}
}