diff --git a/src/api-wrappers/AXUIElement.swift b/src/api-wrappers/AXUIElement.swift index 43b93181..d6c70f16 100644 --- a/src/api-wrappers/AXUIElement.swift +++ b/src/api-wrappers/AXUIElement.swift @@ -28,6 +28,7 @@ extension AXUIElement { // 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)) && + // don't show floating windows isOnNormalLevel() } @@ -119,7 +120,7 @@ extension AXUIElement { func handleSubscriptionAttempt(_ result: AXError, _ axObserver: AXObserver, _ notification: String, _ pointer: UnsafeMutableRawPointer?, _ callback: (() -> Void)?, _ runningApplication: NSRunningApplication?, _ wid: CGWindowID?, _ startTime: DispatchTime) -> Void { if result == .success || result == .notificationAlreadyRegistered { - DispatchQueue.main.async { [weak self] () -> () in + DispatchQueue.main.async { () -> () in callback?() } } else if result != .notificationUnsupported && result != .notImplemented { diff --git a/src/api-wrappers/PrivateApis.swift b/src/api-wrappers/PrivateApis.swift index adaaa155..083a3488 100644 --- a/src/api-wrappers/PrivateApis.swift +++ b/src/api-wrappers/PrivateApis.swift @@ -160,6 +160,16 @@ let kAXFullscreenAttribute = "AXFullScreen" //@_silgen_name("GetProcessPID") //func GetProcessPID(_ psn: inout ProcessSerialNumber, _ pid: inout pid_t) -> Void // +//// returns info about a given psn +//// * macOS 10.9- +//@_silgen_name("GetProcessInformation") @discardableResult +//func GetProcessInformation(_ psn: inout ProcessSerialNumber, _ info: inout ProcessInfoRec) -> OSErr +// +//// returns the psn for a given pid +//// * macOS 10.9- +//@_silgen_name("GetProcessForPID") @discardableResult +//func GetProcessForPID(_ pid: pid_t, _ psn: inout ProcessSerialNumber) -> OSStatus +// //// crashed the app with SIGSEGV //// * macOS 10.10+ //@_silgen_name("CGSGetWindowType") @discardableResult diff --git a/src/logic/Application.swift b/src/logic/Application.swift index 0d143b17..3521cade 100644 --- a/src/logic/Application.swift +++ b/src/logic/Application.swift @@ -1,6 +1,8 @@ import Cocoa class Application: NSObject { + // kvObservers should be listed first, so it gets deinit'ed first; otherwise it can crash + var kvObservers: [NSKeyValueObservation]? var runningApplication: NSRunningApplication var axUiElement: AXUIElement? var axObserver: AXObserver? @@ -26,32 +28,29 @@ class Application: NSObject { init(_ runningApplication: NSRunningApplication) { self.runningApplication = runningApplication super.init() - if runningApplication.isFinishedLaunching { - addAndObserveWindows() - } else { - runningApplication.addObserver(self, forKeyPath: "isFinishedLaunching", options: [.new], context: nil) - } - } - - deinit { - // some apps never finish launching; observer should be removed to avoid leak - removeObserver() + addAndObserveWindows() + kvObservers = [ + runningApplication.observe(\.isFinishedLaunching, options: [.new]) { [weak self] _, _ in self?.addAndObserveWindows() }, + runningApplication.observe(\.activationPolicy, options: [.new]) { [weak self] _, _ in self?.addAndObserveWindows() }, + ] } func removeObserver() { runningApplication.safeRemoveObserver(self, "isFinishedLaunching") } - private func addAndObserveWindows() { - axUiElement = AXUIElementCreateApplication(runningApplication.processIdentifier) - AXObserverCreate(runningApplication.processIdentifier, axObserverCallback, &axObserver) - debugPrint("Adding app", runningApplication.processIdentifier, runningApplication.bundleIdentifier ?? "nil") - observeEvents() + func addAndObserveWindows() { + if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited { + axUiElement = AXUIElementCreateApplication(runningApplication.processIdentifier) + AXObserverCreate(runningApplication.processIdentifier, axObserverCallback, &axObserver) + debugPrint("Adding app", runningApplication.processIdentifier, runningApplication.bundleIdentifier ?? "nil") + observeEvents() + } } func observeNewWindows() { - if let windows = (axUiElement!.windows()? - .filter { $0.isActualWindow(runningApplication.bundleIdentifier) }) { + if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited, + let windows = (axUiElement!.windows()?.filter { $0.isActualWindow(runningApplication.bundleIdentifier) }) { // bug in macOS: sometimes the OS returns multiple duplicate windows (e.g. Mail.app starting at login) let actualWindows = Array(Set(windows.filter { Windows.list.firstIndexThatMatches($0) == nil })) if actualWindows.count > 0 { @@ -60,12 +59,6 @@ class Application: NSObject { } } - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - guard let isFinishedLaunching = change![.newKey], isFinishedLaunching as! Bool else { return } - removeObserver() - addAndObserveWindows() - } - private func addWindows(_ axWindows: [AXUIElement]) { let windows = axWindows.map { Window($0, self) } Windows.list.insertAndScaleRecycledPool(windows, at: 0) diff --git a/src/logic/Applications.swift b/src/logic/Applications.swift index 7b7c5432..fd9b9cdf 100644 --- a/src/logic/Applications.swift +++ b/src/logic/Applications.swift @@ -65,14 +65,7 @@ class Applications { } private static func isActualApplication(_ app: NSRunningApplication) -> Bool { - return (app.activationPolicy != .prohibited || - // Bug in CopyQ; see https://github.com/hluk/CopyQ/issues/1330 - app.bundleIdentifier == "io.github.hluk.CopyQ" || - // Bug in Parsec https://github.com/lwouis/alt-tab-macos/issues/206#issuecomment-609828033 - app.bundleIdentifier == "tv.parsec.www" || - // Bug in Octave.app; see https://github.com/octave-app/octave-app/issues/193#issuecomment-603648857 - app.localizedName == "octave-gui") && - // bug in Octave.app; see https://github.com/octave-app/octave-app/issues/193 - app.bundleIdentifier != "org.octave-app.Octave" + let bundlePackageType = app.bundleURL.flatMap { Bundle(url: $0) }.flatMap { $0.infoDictionary }.flatMap { $0["CFBundlePackageType"] as? String } + return bundlePackageType != "XPC!" } } diff --git a/src/logic/events/WorkspaceEvents.swift b/src/logic/events/WorkspaceEvents.swift index f4fe26db..911270d3 100644 --- a/src/logic/events/WorkspaceEvents.swift +++ b/src/logic/events/WorkspaceEvents.swift @@ -1,23 +1,24 @@ import Cocoa -class WorkspaceEvents: NSObject { - private static var appsObserver = WorkspaceEvents() +class WorkspaceEvents { + private static var appsObserver: NSKeyValueObservation! + private static var previousValueOfRunningApps: Set! static func observeRunningApplications() { - NSWorkspace.shared.addObserver(appsObserver, forKeyPath: "runningApplications", options: [.old, .new], context: nil) + previousValueOfRunningApps = Set(NSWorkspace.shared.runningApplications) + appsObserver = NSWorkspace.shared.observe(\.runningApplications, options: [.old, .new], changeHandler: observerCallback) } - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - let type = NSKeyValueChange(rawValue: change![.kindKey]! as! UInt) - if type == .insertion { - let apps = change![.newKey] as! [NSRunningApplication] - debugPrint("OS event", "apps launched", apps.map { ($0.processIdentifier, $0.bundleIdentifier) }) - Applications.addRunningApplications(apps) - } else if type == .removal { - let apps = change![.oldKey] as! [NSRunningApplication] - debugPrint("OS event", "apps quit", apps.map { ($0.processIdentifier, $0.bundleIdentifier) }) - Applications.removeRunningApplications(apps) - + 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) }) + Applications.addRunningApplications(diff) + } else if change.kind == .removal { + debugPrint("OS event", "apps quit", diff.map { ($0.processIdentifier, $0.bundleIdentifier) }) + Applications.removeRunningApplications(diff) } + previousValueOfRunningApps = workspaceApps } }