feat: improve windows detection

This commit is contained in:
lwouis committed 2025-10-26 16:15:02 +01:00
1 parent 33e5acf0ea
commit cab591eb48
6 files changed
+65 -25

No files matched your search

+7
View File
@@ -181,6 +181,7 @@
BF0C8EF58A07D9752A6392FC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8D928E2ABECB92738113 /* Localizable.strings */; }; BF0C8EF58A07D9752A6392FC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8D928E2ABECB92738113 /* Localizable.strings */; };
BF0C8F303887F8454452F85C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C843F6EF209A70B06FE19 /* Localizable.strings */; }; BF0C8F303887F8454452F85C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C843F6EF209A70B06FE19 /* Localizable.strings */; };
BF0C8F3526BC393290D5BE59 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C81EC1C5F3052A75C7D7B /* InfoPlist.strings */; }; BF0C8F3526BC393290D5BE59 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C81EC1C5F3052A75C7D7B /* InfoPlist.strings */; };
BF0C8F40C2D0FF7601D926D3 /* LightweightTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8363D45FECB8D6F8DCD5 /* LightweightTimer.swift */; };
BF0C8F9B8014AD21E68B1349 /* HelperExtensionsTestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C810FA0F6BC70E5886E11 /* HelperExtensionsTestable.swift */; }; BF0C8F9B8014AD21E68B1349 /* HelperExtensionsTestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C810FA0F6BC70E5886E11 /* HelperExtensionsTestable.swift */; };
BF0C8FF2A4B00D409494EA80 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C88D79FA0C773D94F351A /* InfoPlist.strings */; }; BF0C8FF2A4B00D409494EA80 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C88D79FA0C773D94F351A /* InfoPlist.strings */; };
D04BA004884A273D4D2D3EF1 /* HelperExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD91161791D42FEC4A60 /* HelperExtensions.swift */; }; D04BA004884A273D4D2D3EF1 /* HelperExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD91161791D42FEC4A60 /* HelperExtensions.swift */; };
@@ -388,6 +389,7 @@
BF0C833D471343CEA3DEFE56 /* icon_512x512@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_512x512@2x.png"; sourceTree = "<group>"; }; BF0C833D471343CEA3DEFE56 /* icon_512x512@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_512x512@2x.png"; sourceTree = "<group>"; };
BF0C8343C1F6FCA82552DCB7 /* TableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = "<group>"; }; BF0C8343C1F6FCA82552DCB7 /* TableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = "<group>"; };
BF0C8353097840D7DB1E0E2E /* Markdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Markdown.swift; sourceTree = "<group>"; }; BF0C8353097840D7DB1E0E2E /* Markdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Markdown.swift; sourceTree = "<group>"; };
BF0C8363D45FECB8D6F8DCD5 /* LightweightTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LightweightTimer.swift; sourceTree = "<group>"; };
BF0C83D645B6BB523E50670A /* hr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = hr; path = InfoPlist.strings; sourceTree = "<group>"; }; BF0C83D645B6BB523E50670A /* hr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = hr; path = InfoPlist.strings; sourceTree = "<group>"; };
BF0C83E1E5C670A8B03E3612 /* hr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = hr; path = Localizable.strings; sourceTree = "<group>"; }; BF0C83E1E5C670A8B03E3612 /* hr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = hr; path = Localizable.strings; sourceTree = "<group>"; };
BF0C83EE282A5FB191918799 /* override_contexts_on_poeditor.ts */ = {isa = PBXFileReference; lastKnownFileType = file.ts; path = override_contexts_on_poeditor.ts; sourceTree = "<group>"; }; BF0C83EE282A5FB191918799 /* override_contexts_on_poeditor.ts */ = {isa = PBXFileReference; lastKnownFileType = file.ts; path = override_contexts_on_poeditor.ts; sourceTree = "<group>"; };
@@ -1513,6 +1515,7 @@
BF0C8353097840D7DB1E0E2E /* Markdown.swift */, BF0C8353097840D7DB1E0E2E /* Markdown.swift */,
BF0C88D101728C6798C7FC07 /* private-apis */, BF0C88D101728C6798C7FC07 /* private-apis */,
BF0C810FA0F6BC70E5886E11 /* HelperExtensionsTestable.swift */, BF0C810FA0F6BC70E5886E11 /* HelperExtensionsTestable.swift */,
BF0C8363D45FECB8D6F8DCD5 /* LightweightTimer.swift */,
); );
path = "api-wrappers"; path = "api-wrappers";
sourceTree = "<group>"; sourceTree = "<group>";
@@ -2434,6 +2437,7 @@
BF0C8973EC078169170F2634 /* CliEvents.swift in Sources */, BF0C8973EC078169170F2634 /* CliEvents.swift in Sources */,
BF0C8521DA3078516615C414 /* HelperExtensionsTestable.swift in Sources */, BF0C8521DA3078516615C414 /* HelperExtensionsTestable.swift in Sources */,
BF0C8991399A3277A9A02FFA /* LightImageView.swift in Sources */, BF0C8991399A3277A9A02FFA /* LightImageView.swift in Sources */,
BF0C8F40C2D0FF7601D926D3 /* LightweightTimer.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -3342,6 +3346,7 @@
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "${TEAM_ID}";
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -3433,6 +3438,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = D04BA62D5EACAAAD7BCD3E79 /* release.xcconfig */; baseConfigurationReference = D04BA62D5EACAAAD7BCD3E79 /* release.xcconfig */;
buildSettings = { buildSettings = {
DEVELOPMENT_TEAM = "${TEAM_ID}";
}; };
name = Release; name = Release;
}; };
@@ -3440,6 +3446,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = D04BA1232AFEEFE90D5CC827 /* debug.xcconfig */; baseConfigurationReference = D04BA1232AFEEFE90D5CC827 /* debug.xcconfig */;
buildSettings = { buildSettings = {
DEVELOPMENT_TEAM = "${TEAM_ID}";
}; };
name = Debug; name = Debug;
}; };
+17 -10
View File
@@ -13,7 +13,7 @@ extension AXUIElement {
// if an app times out our AX calls, we retry for 6s then give up // if an app times out our AX calls, we retry for 6s then give up
private static let axCallsRetriesQueueTimeoutInSeconds = Float(6) private static let axCallsRetriesQueueTimeoutInSeconds = Float(6)
// once an app is unresponsive, let's ignore other AX calls for it to avoid congestion // once an app is unresponsive, let's ignore other AX calls for it to avoid congestion
private static var axCallsRetriesQueueUnresponsiveAppsMap = ConcurrentMap<pid_t, UInt64>() private static var axCallsRetriesQueueUnresponsiveAppsMap = ConcurrentMap<String, UInt64>()
// some events like window resizing trigger in quick succession. We debounce those // some events like window resizing trigger in quick succession. We debounce those
private static var eventsDebounceMap = ConcurrentMap<String, DispatchWorkItem>() private static var eventsDebounceMap = ConcurrentMap<String, DispatchWorkItem>()
@@ -21,7 +21,7 @@ extension AXUIElement {
AXUIElementSetMessagingTimeout(AXUIElementCreateSystemWide(), globalMessagingTimeoutInSeconds) AXUIElementSetMessagingTimeout(AXUIElementCreateSystemWide(), globalMessagingTimeoutInSeconds)
} }
enum AXCallType { enum AXCallType: Int {
case subscribeToAppNotification case subscribeToAppNotification
case subscribeToWindowNotification case subscribeToWindowNotification
case subscribeToDockNotification case subscribeToDockNotification
@@ -34,7 +34,7 @@ extension AXUIElement {
/// if the window server is busy, it may not reply to AX calls. We retry right before the call times-out and returns a bogus value /// if the window server is busy, it may not reply to AX calls. We retry right before the call times-out and returns a bogus value
static func retryAxCallUntilTimeout(file: String = #file, function: String = #function, line: Int = #line, context: String = "", after: DispatchTime? = nil, debounceType: DebounceType? = nil, pid: pid_t? = nil, wid: CGWindowID? = nil, retriesQueue: Bool = false, startTimeInNanoseconds: UInt64 = DispatchTime.now().uptimeNanoseconds, callType: AXCallType, block: @escaping () throws -> Void) { static func retryAxCallUntilTimeout(file: String = #file, function: String = #function, line: Int = #line, context: String = "", after: DispatchTime? = nil, debounceType: DebounceType? = nil, pid: pid_t? = nil, wid: CGWindowID? = nil, retriesQueue: Bool = false, startTimeInNanoseconds: UInt64 = DispatchTime.now().uptimeNanoseconds, callType: AXCallType, block: @escaping () throws -> Void) {
let closure = { retryAxCallUntilTimeout_(file: file, function: function, line: line, context: context, after: after, debounceType: debounceType, pid: pid, wid: wid, retriesQueue: retriesQueue, startTimeInNanoseconds: startTimeInNanoseconds, callType: callType, block: block) } let closure = { retryAxCallUntilTimeout_(file: file, function: function, line: line, context: context, after: after, debounceType: debounceType, pid: pid, wid: wid, retriesQueue: retriesQueue, startTimeInNanoseconds: startTimeInNanoseconds, callType: callType, block: block) }
let queue = retriesQueue ? BackgroundWork.axCallsRetriesQueue : BackgroundWork.axCallsFirstAttemptQueue let queue = callType == .updateAppWindows ? BackgroundWork.axCallsManualDiscoveryQueue : (retriesQueue ? BackgroundWork.axCallsRetriesQueue : BackgroundWork.axCallsFirstAttemptQueue)
if let after { if let after {
queue!.addOperationAfter(deadline: after, block: closure) queue!.addOperationAfter(deadline: after, block: closure)
} else if let debounceType, let wid { } else if let debounceType, let wid {
@@ -63,13 +63,14 @@ extension AXUIElement {
// do we already have ongoing retries for this pid? The app is likely unresponsive // do we already have ongoing retries for this pid? The app is likely unresponsive
// if their are common updates, we avoid congestion by only retrying the latest update // if their are common updates, we avoid congestion by only retrying the latest update
if let pid, callType == .updateWindow || callType == .updateAppWindows { if let pid, callType == .updateWindow || callType == .updateAppWindows {
let unresponsiveAppsMapKey = "\(callType.rawValue)\(pid)"
axCallsRetriesQueueUnresponsiveAppsMap.lock.lock() axCallsRetriesQueueUnresponsiveAppsMap.lock.lock()
let time = axCallsRetriesQueueUnresponsiveAppsMap.map[pid] let time = axCallsRetriesQueueUnresponsiveAppsMap.map[unresponsiveAppsMapKey]
axCallsRetriesQueueUnresponsiveAppsMap.lock.unlock() axCallsRetriesQueueUnresponsiveAppsMap.lock.unlock()
if let time { if let time {
if startTimeInNanoseconds > time { if startTimeInNanoseconds > time {
// new most recent call; replace and retry // new most recent call; replace and retry
axCallsRetriesQueueUnresponsiveAppsMap.withLock { $0[pid] = startTimeInNanoseconds } axCallsRetriesQueueUnresponsiveAppsMap.withLock { $0[unresponsiveAppsMapKey] = startTimeInNanoseconds }
} else if startTimeInNanoseconds == time { } else if startTimeInNanoseconds == time {
// most recent call; retry // most recent call; retry
} else { } else {
@@ -78,15 +79,16 @@ extension AXUIElement {
} }
} else { } else {
// first call; set and retry // first call; set and retry
axCallsRetriesQueueUnresponsiveAppsMap.withLock { $0[pid] = startTimeInNanoseconds } axCallsRetriesQueueUnresponsiveAppsMap.withLock { $0[unresponsiveAppsMapKey] = startTimeInNanoseconds }
} }
} }
// should we give up? // should we give up?
let timePassedInSeconds = Float(DispatchTime.now().uptimeNanoseconds - startTimeInNanoseconds) / 1_000_000_000 let timePassedInSeconds = Float(DispatchTime.now().uptimeNanoseconds - startTimeInNanoseconds) / 1_000_000_000
if timePassedInSeconds >= axCallsRetriesQueueTimeoutInSeconds { if timePassedInSeconds >= axCallsRetriesQueueTimeoutInSeconds {
Logger.info("AX call failed for more than \(Int(axCallsRetriesQueueTimeoutInSeconds))s. Giving up on it", logFromContext(file, function, line, context)) Logger.info("AX call failed for more than \(Int(axCallsRetriesQueueTimeoutInSeconds))s. Giving up on it", logFromContext(file, function, line, context))
if let pid { if let pid, callType == .updateWindow || callType == .updateAppWindows {
axCallsRetriesQueueUnresponsiveAppsMap.withLock { $0.removeValue(forKey: pid) } let unresponsiveAppsMapKey = "\(callType.rawValue)\(pid)"
axCallsRetriesQueueUnresponsiveAppsMap.withLock { $0.removeValue(forKey: unresponsiveAppsMapKey) }
} }
return return
} }
@@ -254,6 +256,8 @@ extension AXUIElement {
remoteToken.replaceSubrange(8..<12, with: withUnsafeBytes(of: Int32(0x636f636f)) { Data($0) }) remoteToken.replaceSubrange(8..<12, with: withUnsafeBytes(of: Int32(0x636f636f)) { Data($0) })
var axWindows = [AXUIElement]() var axWindows = [AXUIElement]()
// we iterate to 1000 as a tradeoff between performance, and missing windows of long-lived processes // we iterate to 1000 as a tradeoff between performance, and missing windows of long-lived processes
// different apps can take widely different time for this to complete. We stop iterating if we time out
let timer = LightweightTimer()
for axUiElementId: AXUIElementID in 0..<1000 { for axUiElementId: AXUIElementID in 0..<1000 {
remoteToken.replaceSubrange(12..<20, with: withUnsafeBytes(of: axUiElementId) { Data($0) }) remoteToken.replaceSubrange(12..<20, with: withUnsafeBytes(of: axUiElementId) { Data($0) })
if let axUiElement = _AXUIElementCreateWithRemoteToken(remoteToken as CFData)?.takeRetainedValue(), if let axUiElement = _AXUIElementCreateWithRemoteToken(remoteToken as CFData)?.takeRetainedValue(),
@@ -261,6 +265,9 @@ extension AXUIElement {
[kAXStandardWindowSubrole, kAXDialogSubrole].contains(subrole) { [kAXStandardWindowSubrole, kAXDialogSubrole].contains(subrole) {
axWindows.append(axUiElement) axWindows.append(axUiElement)
} }
if timer.hasElapsed(milliseconds: 100) {
return axWindows
}
} }
return axWindows return axWindows
} }
@@ -474,8 +481,8 @@ enum AxError: Error {
typealias AXUIElementID = UInt64 typealias AXUIElementID = UInt64
enum DebounceType: Int { enum DebounceType: Int {
case windowResizedOrMoved = 0 case windowResizedOrMoved
case windowTitleChanged = 1 case windowTitleChanged
} }
class ConcurrentMap<K: Hashable, V> { class ConcurrentMap<K: Hashable, V> {
+26
View File
@@ -0,0 +1,26 @@
import Darwin
class LightweightTimer {
private static let timebaseRatio: Double = {
var info = mach_timebase_info()
mach_timebase_info(&info)
return Double(info.numer) / Double(info.denom)
}()
private let start: UInt64
var elapsedMilliseconds: Double {
let now = mach_absolute_time()
let nanos = Double(now - start) * Self.timebaseRatio
return nanos / 1_000_000 // ns → ms
}
init() {
start = mach_absolute_time()
}
/// Returns true if the given number of milliseconds has passed
func hasElapsed(milliseconds: Double) -> Bool {
elapsedMilliseconds >= milliseconds
}
}
+10 -11
View File
@@ -177,22 +177,21 @@ class Application: NSObject {
private func observeEvents() { private func observeEvents() {
guard let axObserver else { return } guard let axObserver else { return }
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid, callType: .subscribeToAppNotification) { [weak self] in AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid, callType: .subscribeToAppNotification) { [weak self] in
guard let self else { return } guard let self, !self.isReallyFinishedLaunching else { return }
if try self.axUiElement!.subscribeToNotification(axObserver, Application.notifications.first!) { if try self.axUiElement!.subscribeToNotification(axObserver, Application.notifications.first!) {
Logger.debug("Subscribed to app", self.debugId) Logger.debug("Subscribed to app", self.debugId)
for notification in Application.notifications.dropFirst() { if !self.isReallyFinishedLaunching {
AXUIElement.retryAxCallUntilTimeout(context: self.debugId, pid: self.pid, callType: .subscribeToAppNotification) { [weak self] in
try self?.axUiElement!.subscribeToNotification(axObserver, notification)
}
}
DispatchQueue.main.async { [weak self] in
guard let self else { return }
// some apps have `isFinishedLaunching == true` but are actually not finished, and will return .cannotComplete // some apps have `isFinishedLaunching == true` but are actually not finished, and will return .cannotComplete
// we consider them ready when the first subscription succeeds // we consider them ready when the first subscription succeeds
// windows opened before that point won't send a notification, so check those windows manually here // windows opened before that point won't send a notification, so check those windows manually here
if !self.isReallyFinishedLaunching { self.isReallyFinishedLaunching = true
self.isReallyFinishedLaunching = true for notification in Application.notifications.dropFirst() {
self.manuallyUpdateWindows() AXUIElement.retryAxCallUntilTimeout(context: self.debugId, pid: self.pid, callType: .subscribeToAppNotification) { [weak self] in
try self?.axUiElement!.subscribeToNotification(axObserver, notification)
}
}
DispatchQueue.main.async { [weak self] in
self?.manuallyUpdateWindows()
} }
} }
} }
+4 -4
View File
@@ -7,6 +7,7 @@ class BackgroundWork {
static var accessibilityCommandsQueue: LabeledOperationQueue! static var accessibilityCommandsQueue: LabeledOperationQueue!
static var axCallsFirstAttemptQueue: LabeledOperationQueue! static var axCallsFirstAttemptQueue: LabeledOperationQueue!
static var axCallsRetriesQueue: LabeledOperationQueue! static var axCallsRetriesQueue: LabeledOperationQueue!
static var axCallsManualDiscoveryQueue: LabeledOperationQueue!
static var crashReportsQueue: LabeledOperationQueue! static var crashReportsQueue: LabeledOperationQueue!
// we use Threads to observe events in sequence // we use Threads to observe events in sequence
static var accessibilityEventsThread: BackgroundThreadWithRunLoop! static var accessibilityEventsThread: BackgroundThreadWithRunLoop!
@@ -19,9 +20,6 @@ class BackgroundWork {
private static var totalPotentialThreadCount = 0 private static var totalPotentialThreadCount = 0
static func start() { static func start() {
// screenshots are taken on a serial DispatchQueue. They used to be taken on the .global() concurrent queue.
// it could hang the app since the screenshot OS calls are slow. It would hang or crash with this error:
// >Processes reached dispatch thread soft limit (64)
screenshotsQueue = LabeledOperationQueue("screenshotsQueue", .userInteractive, 8) screenshotsQueue = LabeledOperationQueue("screenshotsQueue", .userInteractive, 8)
// calls to focus/close/minimize/etc windows // calls to focus/close/minimize/etc windows
// They are tried once and if they timeout we don't retry. The OS seems to still execute them even if the call timed out // They are tried once and if they timeout we don't retry. The OS seems to still execute them even if the call timed out
@@ -30,6 +28,8 @@ class BackgroundWork {
// We first try the AX calls on axCallsFirstAttemptQueue. If we get a timeout, we move to axCallsRetriesQueue and retry there for a while // We first try the AX calls on axCallsFirstAttemptQueue. If we get a timeout, we move to axCallsRetriesQueue and retry there for a while
axCallsFirstAttemptQueue = LabeledOperationQueue("axCallsFirstAttemptQueue", .userInteractive, 8) axCallsFirstAttemptQueue = LabeledOperationQueue("axCallsFirstAttemptQueue", .userInteractive, 8)
axCallsRetriesQueue = LabeledOperationQueue("axCallsRetriesQueue", .userInteractive, 8) axCallsRetriesQueue = LabeledOperationQueue("axCallsRetriesQueue", .userInteractive, 8)
// we separate calls to manuallyUpdateWindows since those can be very heavy and numerous
axCallsManualDiscoveryQueue = LabeledOperationQueue("axCallsManualDiscoveryQueue", .userInteractive, 8)
// we observe app and windows notifications. They arrive on this thread, and are handled off the main thread initially // we observe app and windows notifications. They arrive on this thread, and are handled off the main thread initially
accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread", .userInteractive) accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread", .userInteractive)
// we listen to as any keyboard events as possible on a background thread, as it's more available/reliable than the main thread // we listen to as any keyboard events as possible on a background thread, as it's more available/reliable than the main thread
@@ -58,7 +58,7 @@ class BackgroundWork {
static func addPotentialThreadCount(_ additionalCount: Int) { static func addPotentialThreadCount(_ additionalCount: Int) {
totalPotentialThreadCount += additionalCount totalPotentialThreadCount += additionalCount
// a macos process has a soft limit of 64 threads. We need to be careful to don't spawn too many threads through DispatchQueues // a macos process has a soft limit of 64 threads. We need to be careful to don't spawn too many threads through DispatchQueues
assert(totalPotentialThreadCount <= 36) assert(totalPotentialThreadCount <= 43)
} }
// useful during development to inspect how many threads are used by AltTab // useful during development to inspect how many threads are used by AltTab
+1
View File
@@ -239,6 +239,7 @@ class App: AppCenterApplication {
App.app.appIsBeingUsed = true App.app.appIsBeingUsed = true
if isFirstSummon || shortcutIndex != self.shortcutIndex { if isFirstSummon || shortcutIndex != self.shortcutIndex {
NSScreen.updatePreferred() NSScreen.updatePreferred()
Applications.manuallyRefreshAllWindows()
if isVeryFirstSummon { if isVeryFirstSummon {
Windows.sortByLevel() Windows.sortByLevel()
isVeryFirstSummon = false isVeryFirstSummon = false