Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix high CPU usage #1481 #1484

Closed
wants to merge 12 commits into from
60 changes: 56 additions & 4 deletions src/api-wrappers/CGWindowID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,63 @@ extension CGWindowID {
return CGSCopySpacesForWindows(cgsMainConnectionId, CGSSpaceMask.all.rawValue, [self] as CFArray) as! [CGSSpaceID]
}

func screenshot() -> CGImage? {
// CGSHWCaptureWindowList
// fullscreen has multiple windows
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this. I just fullscreen'ed Notes.app, and AltTab shows the same thing I see.

Could you share screenshots to illustrate what you mean by "toolbar window and a main window", and generally what problem this new method fixes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before adding the fullScreenshot method
image

After adding the fullScreenshot method
image

Maybe it depends on machine model. I'm using MBP 16' with Apple Silicon.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it does it on my machine too (still on Catalina). Very cool then! I wonder how many apps really benefit from that nuance though. It's a cool trick, but in the case of Notes.app, it's not very necessary. Maybe some other apps have an important UI element that would get lost though. I worry a bit about the complexity/performance/potential-failure-points of created the composition vs showing the simple image, though. I think it would probably be worth it to first find an app that truly benefits from this refinement before putting it in. What do you think? Especially given that in order to do this trick, we have to rely on CGS API, which has all the problems I mentioned in other comments

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I also took into account the performance issues. In fact, this small issue has little impact, I just made it a little bit optimized as free time. Reminders.app, Hopper Disassembler.app, Xcode.app, Microsoft Word.app, Calendar.app also have this problem as I known.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I tried to use CGWindowListCreateImageFromArray, but it didn't work at all. Maybe I'm not using it the right way.

// e.g. Notes.app has a toolbar window and a main window
// We need to composite these window images
func fullScreenshot(_ win: Window) -> CGImage? {
var height: CGFloat = 0;
var width: CGFloat = 0;
var imageMap = [(CGWindow, CGImage)]()
var maxWidthWindowId: CGWindowID = 0
let screen = Spaces.spaceFrameMap.first { $0.0 == win.spaceId }!.1
var windowsInSpaces = Spaces.windowsInSpaces([win.spaceId]) // The returned windows are sorted from highest to lowest according to the z-index
var windowsToCapture: [CGWindow] = []
// find current app's window in the fullscreen space
for item in windowsInSpaces {
let cgWin = CGWindowListCopyWindowInfo([.excludeDesktopElements, .optionIncludingWindow], item) as! [CGWindow]
guard cgWin.first!.isNotMenubarOrOthers(),
cgWin.first!.ownerPID() == win.application.runningApplication.processIdentifier,
cgWin.first!.bounds() != nil,
let bounds = CGRect(dictionaryRepresentation: cgWin.first!.bounds()!), bounds.height > 0, bounds.width > 0 else { continue }
windowsToCapture.append(cgWin.first!)
}
// Drawing images from lowest to highest base on the z-index
windowsToCapture = windowsToCapture.reversed()
for item in windowsToCapture {
let bounds = CGRect(dictionaryRepresentation: item.bounds()!)
if width < bounds!.width {
maxWidthWindowId = item.id()!
}
var windowId = item.id()!
let list = CGSHWCaptureWindowList(cgsMainConnectionId, &windowId, 1, [.ignoreGlobalClipShape, .nominalResolution]).takeRetainedValue() as! [CGImage]
imageMap.append((item, list.first!))
}
let bytesPerRow = imageMap.first { $0.0.id()! == maxWidthWindowId }!.1.bytesPerRow
var context = CGContext.init(data: nil,
width: Int(screen.frame.width),
height: Int(screen.frame.height),
bitsPerComponent: 8,
bytesPerRow: bytesPerRow,
space: imageMap.first!.1.colorSpace!,
bitmapInfo: imageMap.first!.1.bitmapInfo.rawValue)
// composite these window images
for item in imageMap {
let bounds = CGRect(dictionaryRepresentation: item.0.bounds()!)
// Convert the coordinate system, the origin of window is top-left, the image is bottom-left
// so we need to convert y-index
context?.draw(item.1, in: CGRect.init(x: bounds!.origin.x, y: screen.frame.height - bounds!.height - bounds!.origin.y, width: bounds!.width, height: bounds!.height))
}
return context?.makeImage()
}

func screenshot(_ win: Window) -> CGImage? {
var windowId_ = self
let list = CGSHWCaptureWindowList(cgsMainConnectionId, &windowId_, 1, [.ignoreGlobalClipShape, .nominalResolution]).takeRetainedValue() as! [CGImage]
return list.first
if win.isFullscreen {
return fullScreenshot(win)
} else {
let list = CGSHWCaptureWindowList(cgsMainConnectionId, &windowId_, 1, [.ignoreGlobalClipShape, .nominalResolution]).takeRetainedValue() as! [CGImage]
return list.first
}

// // CGWindowListCreateImage
// return CGWindowListCreateImage(.null, .optionIncludingWindow, self, [.boundsIgnoreFraming, .bestResolution])
Expand Down
7 changes: 6 additions & 1 deletion src/api-wrappers/PrivateApis.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,15 @@ func CGSCopyWindowsWithOptionsAndTags(_ cid: CGSConnectionID, _ owner: UInt32, _
func CGSManagedDisplayGetCurrentSpace(_ cid: CGSConnectionID, _ displayUuid: CFString) -> CGSSpaceID

// adds the provided windows to the provided spaces
// * macOS 10.10+
// * macOS 10.10-12.2
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RIP 😄

@_silgen_name("CGSAddWindowsToSpaces")
func CGSAddWindowsToSpaces(_ cid: CGSConnectionID, _ windows: NSArray, _ spaces: NSArray) -> Void

// Move the given windows (CGWindowIDs) to the given space (CGSSpaceID)
// * macOS 10.10+
@_silgen_name("CGSMoveWindowsToManagedSpace")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work exactly like CGSAddWindowsToSpaces? I remember testing almost every API from https://github.com/phracker/MacOSX-SDKs/blob/10dd4868459aed5c4e6a0f8c9db51e20a5677a6b/MacOSX10.10.sdk/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics.tbd which had the word "space" or "window" in it. I'm surprised that I missed this one. Are you sure it works in every scenario (e.g. multiple monitors, minimized window, hidden app, etc)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This api will move the window from the original space to another space. It means the window will disappear from the original space and show on another space. I did not test the scenarios you mentioned. I just use this api to move the invisible window to the space we want to. I will do some tests. By the way, I can not do the tests for multiple monitors, because I only have one monitor.😂

func CGSMoveWindowsToManagedSpace(_ cid: CGSConnectionID, _ windows: NSArray, _ space: CGSSpaceID) -> Void

// remove the provided windows from the provided spaces
// * macOS 10.10+
@_silgen_name("CGSRemoveWindowsFromSpaces")
Expand Down
36 changes: 36 additions & 0 deletions src/logic/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,42 @@ class Application: NSObject {
return windows
}

func getOtherSpaceWindows(_ windowsOnlyOnOtherSpaces: [CGWindowID]) -> [Window] {
var otherSpaceWindows: [Window] = []
for winId in windowsOnlyOnOtherSpaces {
let cgWinArray = CGWindowListCopyWindowInfo([.excludeDesktopElements, .optionIncludingWindow], winId) as! [CGWindow]
// get current app's windows only on other space
guard runningApplication.processIdentifier == cgWinArray.first!.ownerPID()
&& cgWinArray.first!.id() != nil
&& cgWinArray.first!.isNotMenubarOrOthers()
&& cgWinArray.first!.bounds() != nil
&& CGRect(dictionaryRepresentation: cgWinArray.first!.bounds()!)!.width > 100
&& CGRect(dictionaryRepresentation: cgWinArray.first!.bounds()!)!.height > 100
else { continue }
guard let capture = CGWindowListCreateImage(CGRect.null, .optionIncludingWindow, cgWinArray.first!.id()!, .boundsIgnoreFraming) else { continue }
let win = Window(self, cgWinArray.first!)
Windows.appendAndUpdateFocus(win)
otherSpaceWindows.append(win)
}
return otherSpaceWindows
}

func addOtherSpaceWindows(_ windowsOnlyOnOtherSpaces: [CGWindowID]) {
if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited {
retryAxCallUntilTimeout { [weak self] in
guard let self = self else { return }
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
var windows = self.getOtherSpaceWindows(windowsOnlyOnOtherSpaces)
if let window = self.addWindowslessAppsIfNeeded() {
windows.append(contentsOf: window)
}
App.app.refreshOpenUi(windows)
}
}
}
}

func addWindowslessAppsIfNeeded() -> [Window]? {
if !Preferences.hideWindowlessApps &&
runningApplication.activationPolicy == .regular &&
Expand Down
25 changes: 20 additions & 5 deletions src/logic/Applications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ class Applications {
_ = group.wait(wallTimeout: .now() + .seconds(2))
}

static func addOtherSpaceWindows(_ windowsOnlyOnOtherSpaces: [CGWindowID]) {
for app in list {
app.wasLaunchedBeforeAltTab = true
guard app.runningApplication.isFinishedLaunching else { continue }
app.addOtherSpaceWindows(windowsOnlyOnOtherSpaces)
}
}

static func initialDiscovery() {
addInitialRunningApplications()
addInitialRunningApplicationsWindows()
Expand All @@ -32,10 +40,11 @@ class Applications {
let windowsOnOtherSpaces = Spaces.windowsInSpaces(otherSpaces)
let windowsOnlyOnOtherSpaces = Array(Set(windowsOnOtherSpaces).subtracting(windowsOnCurrentSpace))
if windowsOnlyOnOtherSpaces.count > 0 {
// on initial launch, we use private APIs to bring windows from other spaces into the current space, observe them, then remove them from the current space
CGSAddWindowsToSpaces(cgsMainConnectionId, windowsOnlyOnOtherSpaces as NSArray, [Spaces.currentSpaceId])
Applications.observeNewWindowsBlocking()
CGSRemoveWindowsFromSpaces(cgsMainConnectionId, windowsOnlyOnOtherSpaces as NSArray, [Spaces.currentSpaceId])
// Currently we add those window in other space without AXUIElement init
// We don't need to get the AXUIElement until we focus these windows.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're correct that CGSMoveWindowsToManagedSpace is a perfect replacement to CGSAddWindowsToSpaces, then we could at least fix the 12.3 issue with CGSAddWindowsToSpaces not working, but replacing the call to CGSMoveWindowsToManagedSpace. That would fix the issue. Then, separately, we can have the conversation about the invisible window trick, and how to move forward there

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see my follow-up here: #1324 (comment)

// when we need to focus these windows, we use the helper window to take us to that space,
// then get the AXUIElement, and finally focus that window.
Applications.addOtherSpaceWindows(windowsOnlyOnOtherSpaces)
}
}
}
Expand Down Expand Up @@ -115,7 +124,13 @@ class Applications {
// 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 {
guard let winApp = (allWindows.first { app.processIdentifier == $0.ownerPID()
&& $0.isNotMenubarOrOthers()
&& $0.id() != nil
&& $0.bounds() != nil
&& CGRect(dictionaryRepresentation: $0.bounds()!)!.width > 0
&& CGRect(dictionaryRepresentation: $0.bounds()!)!.height > 0
}) else {
Comment on lines +127 to +133
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said in the other ticket, you're re-implementing a mirror to

static func isActualWindow(_ runningApp: NSRunningApplication, _ wid: CGWindowID, _ level: CGWindowLevel, _ title: String?, _ subrole: String?, _ role: String?, _ size: CGSize?) -> Bool {
here. This will not be good enough. AltTab will thus show windows it shouldn't show.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I add these codes just to filter those apps which have no windows(e.g. com.apple.universalcontroller). This is for those apps launched before AltTab. At that time, we haven't go to AXUIElement.isActualWindow. The code is just there to allow us to avoid observing to applications we are not interested in and to avoid wasting CPU time on subscribeToNotification retries.

return false
}
return true
Expand Down
18 changes: 18 additions & 0 deletions src/logic/HelperWindow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Cocoa
/**
* we use this window to help us switch to another space
*/
class HelperWindow: NSWindow {
var canBecomeKey_ = true
override var canBecomeKey: Bool { canBecomeKey_ }
convenience init() {
self.init(contentRect: .zero, styleMask: [.borderless], backing: .buffered, defer: false)
setupWindow()
}

private func setupWindow() {
isReleasedWhenClosed = false
hidesOnDeactivate = false
title = "Helper Window"
}
}
16 changes: 15 additions & 1 deletion src/logic/Spaces.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,25 @@ class Spaces {
static var visibleSpaces = [CGSSpaceID]()
static var screenSpacesMap = [ScreenUuid: [CGSSpaceID]]()
static var idsAndIndexes = [(CGSSpaceID, SpaceIndex)]()
static var spaceFrameMap = [(CGSSpaceID, NSScreen)]()

static func observeSpaceChanges() {
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.activeSpaceDidChangeNotification, object: nil, queue: nil, using: { _ in
debugPrint("OS event", "activeSpaceDidChangeNotification")
refreshAllIdsAndIndexes()
updateCurrentSpace()
// if UI was kept open during Space transition, the Spaces may be obsolete; we refresh them
Windows.list.forEachAsync { $0.updatesWindowSpace() }
Windows.list.forEachAsync {
$0.updatesWindowSpace()
// we need to set up AXUIElement for those invisiable window launched before AltTab when space changed
if $0.axUiElement == nil && $0.spaceId == currentSpaceId && !$0.isWindowlessApp {
do {
try $0.getAxUiElementAndObserveEvents()
} catch {
debugPrint("can not setUpMissingInfoForOtherWindows for", $0.application.runningApplication.bundleIdentifier)
}
}
}
})
NSWorkspace.shared.notificationCenter.addObserver(forName: NSApplication.didChangeScreenParametersNotification, object: nil, queue: nil, using: { _ in
debugPrint("OS event", "didChangeScreenParametersNotification")
Expand Down Expand Up @@ -47,15 +58,18 @@ class Spaces {
idsAndIndexes.removeAll()
screenSpacesMap.removeAll()
visibleSpaces.removeAll()
spaceFrameMap.removeAll()
var spaceIndex = SpaceIndex(1)
(CGSCopyManagedDisplaySpaces(cgsMainConnectionId) as! [NSDictionary]).forEach { (screen: NSDictionary) in
var display = screen["Display Identifier"] as! ScreenUuid
if display as String == "Main", let mainUuid = NSScreen.main?.uuid() {
display = mainUuid
}
let nsScreen = NSScreen.screens.first { $0.uuid() == display } as! NSScreen
(screen["Spaces"] as! [NSDictionary]).forEach { (space: NSDictionary) in
let spaceId = space["id64"] as! CGSSpaceID
idsAndIndexes.append((spaceId, spaceIndex))
spaceFrameMap.append((spaceId, nsScreen))
screenSpacesMap[display, default: []].append(spaceId)
spaceIndex += 1
}
Expand Down
76 changes: 72 additions & 4 deletions src/logic/Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,44 @@ class Window {
debugPrint("Adding app-window", title ?? "nil", application.runningApplication.bundleIdentifier ?? "nil")
}

// we init a window when this window is not at current active space
init(_ application: Application, _ cgWindow: CGWindow) {
self.application = application
self.title = bestEffortTitle(cgWindow.title())
self.cgWindowId = cgWindow.id()!
let bounds = CGRect(dictionaryRepresentation: cgWindow.bounds()!)
self.size = bounds!.size
self.position = bounds!.origin
updatesWindowSpace()
if CGSSpaceGetType(cgsMainConnectionId, self.spaceId) == .fullscreen {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another worry with using CGS APIs instead of AX APIs is that I don't know if there are delays when calling it. AX has lots of problems like it will block for a long time if the app is busy or the system is busy. We have systems to retry on a loop to eventually get the info. It tooks years and is pretty elaborate and deals with all sorts of edge-cases. Here we are making a simple/naive call to some CGS API. We are making the call on the main thread, which means that any delay from that API will make AltTab UI stutter/freeze. The AX calls are always happening on another thread so that AltTab never freezes. It's a nightmare of concurrent programming but it was the only way to get rid of lag/freeze/unresponsiveness, and it's the reason AltTab is (mostly) very responsive, regardless of how laggy the OS is at responding to its APIs.

self.isFullscreen = true
} else {
self.isFullscreen = false
}
self.isMinimized = false
if !Preferences.hideThumbnails {
refreshThumbnail()
}
application.removeWindowslessAppWindow()
debugPrint("Adding app-window", title ?? "nil", application.runningApplication.bundleIdentifier ?? "nil")
}

func getAxUiElementAndObserveEvents() throws {
guard let windows = try self.application.axUiElement?.windows(), windows.count > 0 else {
debugPrint("try to getAxUiElementAndObserveEvents nil", self.application.runningApplication.bundleIdentifier, self.title, self.spaceId)
return
}
for win in windows {
if try cgWindowId == win.cgWindowId() {
self.axUiElement = win
self.isMinimized = try self.axUiElement.isMinimized()
self.observeEvents()
return
}
}
debugPrint("try to getAxUiElementAndObserveEvents failed", self.application.runningApplication.bundleIdentifier, self.title, self.spaceId)
}

deinit {
debugPrint("Deinit window", title ?? "nil", application.runningApplication.bundleIdentifier ?? "nil")
}
Expand All @@ -83,13 +121,13 @@ class Window {
}

func refreshThumbnail() {
guard let cgImage = cgWindowId.screenshot() else { return }
guard let cgImage = cgWindowId.screenshot(self) else { return }
thumbnail = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
thumbnailFullSize = thumbnail!.size
}

func close() {
if isWindowlessApp { return }
if isWindowlessApp || self.axUiElement == nil { return }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean we can't close a window if we haven't visited its Space? That not ideal...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a pity that HyperSwitch also can't do that either.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested and it can.

I did this to test:

  • Opened a new window
  • Send it to another Space
  • Launch HyperSwitch
  • Press alt-tab
  • Notice that HS shows the window
  • Hover the mouse over the window in HS
  • A close button appears, press it
  • The window on the other Space is closer, even though I never navigated to that Space

How can HS close the window without the AX reference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On macOS 12.3.1, I did the steps same to you, but HyperSwitch just took me to that space and didn't close the window. Maybe the private APIs they used also broken.

BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
guard let self = self else { return }
if self.isFullscreen {
Expand All @@ -102,7 +140,7 @@ class Window {
}

func minDemin() {
if isWindowlessApp { return }
if isWindowlessApp || self.axUiElement == nil { return }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above

BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
guard let self = self else { return }
if self.isFullscreen {
Expand All @@ -119,7 +157,7 @@ class Window {
}

func toggleFullscreen() {
if isWindowlessApp { return }
if isWindowlessApp || self.axUiElement == nil { return }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above

BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
guard let self = self else { return }
self.axUiElement.setAttribute(kAXFullscreenAttribute, !self.isFullscreen)
Expand All @@ -145,13 +183,43 @@ class Window {
}
}

func gotoSpace(_ spaceId: CGSSpaceID) {
CGSMoveWindowsToManagedSpace(cgsMainConnectionId, [App.app.helperWindow.windowNumber] as NSArray, spaceId)
App.app.showHelperWindow()
}

func focus() {
if isWindowlessApp {
if let bundleID = application.runningApplication.bundleIdentifier {
NSWorkspace.shared.launchApplication(withBundleIdentifier: bundleID, additionalEventParamDescriptor: nil, launchIdentifier: nil)
} else {
application.runningApplication.activate(options: .activateIgnoringOtherApps)
}
} else if self.axUiElement == nil {
// the window is in other space, we can not get AXUIElement before we go to it's space
// when we want to focus the window, it means we want to go to the space and focus the window
// so we just go to that space by using helperWindow, then we get AXUIElement and focus the window
gotoSpace(self.spaceId)
do {
retryAxCallUntilTimeout { [weak self] in
guard let self = self else { return }
try self.getAxUiElementAndObserveEvents()
if self.axUiElement == nil {
// retry until we get axUiElement
throw AxError.runtimeError
}
BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
guard let self = self else { return }
var psn = ProcessSerialNumber()
GetProcessForPID(self.application.pid, &psn)
_SLPSSetFrontProcessWithOptions(&psn, self.cgWindowId, .userGenerated)
self.makeKeyWindow(psn)
self.axUiElement.focusWindow()
}
}
} catch {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think exceptions can bubble up to here. retryAxCallUntilTimeout happens on another thread, and absorbs exceptions within

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, during the testing process, it was found that this happens. Because of the animation of the space switch, it causes us not to get the axUiElement?.windows() right away, so we need to retry several times.

debugPrint("can not setUpMissingInfoForCurrentWindow for", self.application.runningApplication.bundleIdentifier)
}
} else {
// macOS bug: when switching to a System Preferences window in another space, it switches to that space,
// but quickly switches back to another window in that space
Expand Down
7 changes: 7 additions & 0 deletions src/ui/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class App: AppCenterApplication, NSApplicationDelegate {
static var app: App!
var thumbnailsPanel: ThumbnailsPanel!
var preferencesWindow: PreferencesWindow!
var helperWindow: HelperWindow!
var feedbackWindow: FeedbackWindow!
var isFirstSummon = true
var appIsBeingUsed = false
Expand Down Expand Up @@ -59,6 +60,7 @@ class App: AppCenterApplication, NSApplicationDelegate {
Spaces.initialDiscovery()
Applications.initialDiscovery()
self.preferencesWindow = PreferencesWindow()
self.helperWindow = HelperWindow()
self.feedbackWindow = FeedbackWindow()
KeyboardEvents.addEventHandlers()
MouseEvents.observe()
Expand Down Expand Up @@ -158,6 +160,11 @@ class App: AppCenterApplication, NSApplicationDelegate {
showSecondaryWindow(preferencesWindow)
}

// focus the helper window and we can go to that space
@objc func showHelperWindow() {
showSecondaryWindow(helperWindow)
}

func showSecondaryWindow(_ window: NSWindow?) {
if let window = window {
NSScreen.preferred().repositionPanel(window, .appleCentered)
Expand Down