From 804b7e2aa4e8d4285b2f47b3162cc66102aedb11 Mon Sep 17 00:00:00 2001 From: Louis Pontoise Date: Wed, 13 May 2020 12:16:31 +0900 Subject: [PATCH] fix: duplicate windows shown after login (closes #292) --- src/api-wrappers/AXUIElement.swift | 3 ++- src/api-wrappers/HelperExtensions.swift | 6 +++--- src/logic/Application.swift | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/api-wrappers/AXUIElement.swift b/src/api-wrappers/AXUIElement.swift index b4851b62..43b93181 100644 --- a/src/api-wrappers/AXUIElement.swift +++ b/src/api-wrappers/AXUIElement.swift @@ -16,13 +16,14 @@ extension AXUIElement { } func isActualWindow(_ bundleIdentifier: 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 subrole_ = subrole() - return subrole_ != nil && + return cgWindowId() != 0 && subrole_ != nil && (["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 diff --git a/src/api-wrappers/HelperExtensions.swift b/src/api-wrappers/HelperExtensions.swift index 62dbe57a..1e53caf9 100644 --- a/src/api-wrappers/HelperExtensions.swift +++ b/src/api-wrappers/HelperExtensions.swift @@ -17,9 +17,9 @@ extension NSObject { extension Array where Element == Window { func firstIndexThatMatches(_ element: AXUIElement) -> Self.Index? { - // == is safer than comparing `CGWindowID` because it will succeed even if the window is deallocated - // by the OS, in which case the `CGWindowID` will be `-1` - return firstIndex(where: { $0.axUiElement == element }) + // the window can be deallocated by the OS, in which case its `CGWindowID` will be `-1` + // we check for equality both on the AXUIElement, and the CGWindowID, in order to catch all scenarios + return firstIndex(where: { $0.axUiElement == element || ($0.cgWindowId != -1 && $0.cgWindowId == element.cgWindowId()) }) } mutating func insertAndScaleRecycledPool(_ elements: [Element], at i: Int) { diff --git a/src/logic/Application.swift b/src/logic/Application.swift index 0ad60a34..0d143b17 100644 --- a/src/logic/Application.swift +++ b/src/logic/Application.swift @@ -52,7 +52,8 @@ class Application: NSObject { func observeNewWindows() { if let windows = (axUiElement!.windows()? .filter { $0.isActualWindow(runningApplication.bundleIdentifier) }) { - let actualWindows = windows.filter { Windows.list.firstIndexThatMatches($0) == nil } + // 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 { addWindows(actualWindows) }