feat: improve performance when many windows are open (closes #5177)
BREAKING CHANGE: major performance improvements
This commit is contained in:
1 parent
4b830e6ba7
commit
5d0a92f3f7
8 files changed
+46
-38
No files matched your search
@@ -147,7 +147,7 @@ class Application: NSObject {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
if self.addWindowlessWindowIfNeeded() != nil {
|
||||
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
|
||||
App.app.refreshOpenUiAfterExternalEvent([])
|
||||
}
|
||||
}
|
||||
// workaround: some apps launch but take a while to create their window(s)
|
||||
@@ -175,14 +175,14 @@ class Application: NSObject {
|
||||
let window = Window(self)
|
||||
Windows.appendWindow(window)
|
||||
focusedWindow = nil
|
||||
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
|
||||
App.app.refreshOpenUiAfterExternalEvent([])
|
||||
return window
|
||||
}
|
||||
|
||||
func removeWindowlessAppWindow() {
|
||||
guard let windowlessAppWindow = (Windows.list.first { $0.isWindowlessApp == true && $0.application.pid == pid }) else { return }
|
||||
Windows.removeWindows([windowlessAppWindow], false)
|
||||
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
|
||||
App.app.refreshOpenUiAfterExternalEvent([])
|
||||
}
|
||||
|
||||
func hideOrShow() {
|
||||
|
||||
@@ -76,7 +76,7 @@ class Applications {
|
||||
// comparing pid here can fail here, as it can be already nil; we use isEqual here to avoid the issue
|
||||
list.removeAll { $0.runningApplication.isEqual(tApp) }
|
||||
}
|
||||
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
|
||||
App.app.refreshOpenUiAfterExternalEvent([])
|
||||
}
|
||||
|
||||
static func refreshBadgesAsync() {
|
||||
|
||||
@@ -112,7 +112,7 @@ class Window {
|
||||
view.thumbnail.updateContents(screenshot, thumbnailSize)
|
||||
// if the thumbnail size has changed, we need to refresh the open UI
|
||||
if newSize {
|
||||
App.app.refreshOpenUi([], .refreshOnlyThumbnailsAfterShowUi)
|
||||
App.app.refreshOpenUiAfterExternalEvent([])
|
||||
}
|
||||
}
|
||||
App.app.previewPanel.updateIfShowing(cgWindowId, screenshot, position, size)
|
||||
|
||||
@@ -402,7 +402,7 @@ class Windows {
|
||||
if addWindowlessWindowIfNeeded {
|
||||
windows.forEach { $0.application.addWindowlessWindowIfNeeded() }
|
||||
}
|
||||
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent, windowRemoved: true)
|
||||
App.app.refreshOpenUiAfterExternalEvent([], windowRemoved: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class AccessibilityEvents {
|
||||
App.app.checkIfShortcutsShouldBeDisabled(nil, app)
|
||||
if let windowless = (Windows.list.first { $0.isWindowlessApp && $0.application.pid == pid }) {
|
||||
if let windows = Windows.updateLastFocusOrder(windowless) {
|
||||
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
|
||||
App.app.refreshOpenUiAfterExternalEvent(windows)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ class AccessibilityEvents {
|
||||
// if we process the "shown" event too fast, the window won't be listed by CGSCopyWindowsWithOptionsAndTags
|
||||
// it will thus be detected as isTabbed. We add a delay to work around this scenario
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
|
||||
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
|
||||
App.app.refreshOpenUiAfterExternalEvent(windows)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ class AccessibilityEvents {
|
||||
} else if type == kAXWindowResizedNotification || type == kAXWindowMovedNotification {
|
||||
windowResizedOrMoved(window)
|
||||
} else {
|
||||
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
|
||||
App.app.refreshOpenUiAfterExternalEvent([window])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,12 +122,12 @@ class AccessibilityEvents {
|
||||
window.application.focusedWindow = window
|
||||
App.app.checkIfShortcutsShouldBeDisabled(window, nil)
|
||||
if let windows = Windows.updateLastFocusOrder(window) {
|
||||
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
|
||||
App.app.refreshOpenUiAfterExternalEvent(windows)
|
||||
}
|
||||
}
|
||||
|
||||
private static func windowResizedOrMoved(_ window: Window) {
|
||||
window.updateSpacesAndScreen()
|
||||
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
|
||||
App.app.refreshOpenUiAfterExternalEvent([window])
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ final class Debouncer {
|
||||
}
|
||||
// ScreensEvents: a screen added or removed can shuffle windows around Spaces; we refresh them
|
||||
// SpacesEvents: if UI was kept open during Space transition, the Spaces may be obsolete; we refresh them
|
||||
App.app.refreshOpenUi(Windows.list, .refreshUiAfterExternalEvent)
|
||||
App.app.refreshOpenUiAfterExternalEvent(Windows.list)
|
||||
Logger.info { "screens:\(NSScreen.screens.map { ($0.uuid() ?? "nil" as CFString, $0.frame) })" }
|
||||
Logger.info { "currentSpace:\(Spaces.currentSpaceIndex) (id:\(Spaces.currentSpaceId)) spaces:\(Spaces.screenSpacesMap)" }
|
||||
})
|
||||
|
||||
+33
-14
@@ -32,6 +32,8 @@ class App: AppCenterApplication {
|
||||
private var appCenterDelegate: AppCenterCrash?
|
||||
// don't queue multiple delayed rebuildUi() calls
|
||||
private var delayedDisplayScheduled = 0
|
||||
private var lastRefreshTimeInNanoseconds = DispatchTime.now().uptimeNanoseconds
|
||||
private var nextRefreshScheduled = false
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
@@ -199,24 +201,43 @@ class App: AppCenterApplication {
|
||||
CGWarpMouseCursorPosition(point)
|
||||
}
|
||||
|
||||
func refreshOpenUi(_ windowsToScreenshot: [Window], _ source: RefreshCausedBy, windowRemoved: Bool = false) {
|
||||
Windows.refreshThumbnailsAsync(windowsToScreenshot, source, windowRemoved: windowRemoved)
|
||||
guard appIsBeingUsed else { return }
|
||||
if source == .refreshUiAfterExternalEvent {
|
||||
if !Windows.updatesBeforeShowing() { hideUi(); return }
|
||||
func refreshOpenUiAfterExternalEvent(_ windowsToScreenshot: [Window], windowRemoved: Bool = false) {
|
||||
Windows.refreshThumbnailsAsync(windowsToScreenshot, .refreshUiAfterExternalEvent, windowRemoved: windowRemoved)
|
||||
refreshOpenUiWithThrottling {
|
||||
guard self.appIsBeingUsed else { return }
|
||||
if !Windows.updatesBeforeShowing() { self.hideUi(); return }
|
||||
self.refreshUi()
|
||||
}
|
||||
guard appIsBeingUsed else { return }
|
||||
}
|
||||
|
||||
func refreshUi() {
|
||||
guard self.appIsBeingUsed else { return }
|
||||
Windows.updateSelectedWindow()
|
||||
guard appIsBeingUsed else { return }
|
||||
thumbnailsPanel.updateContents()
|
||||
guard appIsBeingUsed else { return }
|
||||
guard self.appIsBeingUsed else { return }
|
||||
self.thumbnailsPanel.updateContents()
|
||||
guard self.appIsBeingUsed else { return }
|
||||
Windows.voiceOverWindow() // at this point ThumbnailViews are assigned to the window, and ready
|
||||
guard appIsBeingUsed else { return }
|
||||
guard self.appIsBeingUsed else { return }
|
||||
Windows.previewSelectedWindowIfNeeded()
|
||||
guard appIsBeingUsed else { return }
|
||||
guard self.appIsBeingUsed else { return }
|
||||
Applications.refreshBadgesAsync()
|
||||
}
|
||||
|
||||
func refreshOpenUiWithThrottling( _ block: @escaping () -> Void) {
|
||||
let timeSinceLastRefreshInSeconds = Float(DispatchTime.now().uptimeNanoseconds - lastRefreshTimeInNanoseconds) / 1_000_000
|
||||
if timeSinceLastRefreshInSeconds >= 200 {
|
||||
lastRefreshTimeInNanoseconds = DispatchTime.now().uptimeNanoseconds
|
||||
block()
|
||||
return
|
||||
}
|
||||
guard !nextRefreshScheduled else { return }
|
||||
nextRefreshScheduled = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.210) {
|
||||
self.nextRefreshScheduled = false
|
||||
self.refreshOpenUiWithThrottling(block)
|
||||
}
|
||||
}
|
||||
|
||||
func showUiOrCycleSelection(_ shortcutIndex: Int, _ forceDoNothingOnRelease_: Bool) {
|
||||
forceDoNothingOnRelease = forceDoNothingOnRelease_
|
||||
Logger.debug { "isFirstSummon:\(self.isFirstSummon) shortcutIndex:\(shortcutIndex)" }
|
||||
@@ -252,7 +273,7 @@ class App: AppCenterApplication {
|
||||
guard appIsBeingUsed else { return }
|
||||
Appearance.update()
|
||||
guard appIsBeingUsed else { return }
|
||||
refreshOpenUi([], .showUi)
|
||||
refreshUi()
|
||||
guard appIsBeingUsed else { return }
|
||||
thumbnailsPanel.show()
|
||||
KeyRepeatTimer.startRepeatingKeyNextWindow()
|
||||
@@ -345,8 +366,6 @@ extension App: NSApplicationDelegate {
|
||||
}
|
||||
|
||||
enum RefreshCausedBy {
|
||||
case showUi
|
||||
case refreshOnlyThumbnailsAfterShowUi
|
||||
case refreshUiAfterThumbnailsHaveBeenRefreshed
|
||||
case refreshUiAfterExternalEvent
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import Cocoa
|
||||
class ThumbnailsPanel: NSPanel {
|
||||
var thumbnailsView = ThumbnailsView()
|
||||
override var canBecomeKey: Bool { true }
|
||||
private var didDisplayOnce = false
|
||||
static var maxPossibleThumbnailSize = NSSize.zero
|
||||
static var maxPossibleAppIconSize = NSSize.zero
|
||||
|
||||
@@ -45,7 +44,6 @@ class ThumbnailsPanel: NSPanel {
|
||||
}
|
||||
|
||||
override func orderOut(_ sender: Any?) {
|
||||
didDisplayOnce = false
|
||||
if Preferences.fadeOutAnimation {
|
||||
NSAnimationContext.runAnimationGroup(
|
||||
{ _ in animator().alphaValue = 0 },
|
||||
@@ -56,16 +54,6 @@ class ThumbnailsPanel: NSPanel {
|
||||
}
|
||||
}
|
||||
|
||||
override func displayIfNeeded() {
|
||||
super.displayIfNeeded()
|
||||
if !didDisplayOnce {
|
||||
didDisplayOnce = true
|
||||
DispatchQueue.main.async {
|
||||
Applications.manuallyRefreshAllWindows()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func show() {
|
||||
updateAppearance()
|
||||
alphaValue = 1
|
||||
@@ -134,6 +122,7 @@ extension ThumbnailsPanel: NSWindowDelegate {
|
||||
// this avoids command+q from quitting AltTab itself, or command+p from printing
|
||||
DispatchQueue.main.async {
|
||||
MainMenu.toggle(enabled: false)
|
||||
Applications.manuallyRefreshAllWindows()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user