diff --git a/src/logic/events/ScreensEvents.swift b/src/logic/events/ScreensEvents.swift index d9ede230..3d562b39 100644 --- a/src/logic/events/ScreensEvents.swift +++ b/src/logic/events/ScreensEvents.swift @@ -1,19 +1,52 @@ import Cocoa class ScreensEvents { + static let debouncerScreenAndSpace = Debouncer(.main) + static func observe() { NotificationCenter.default.addObserver(self, selector: #selector(handleEvent), name: NSApplication.didChangeScreenParametersNotification, object: nil) } @objc private static func handleEvent(_ notification: Notification) { - Logger.debug { notification.name.rawValue } - Spaces.refresh() - // a screen added or removed, or screen resolution change can mess up layout; we reset components - App.app.resetPreferencesDependentComponents() - // a screen added or removed can shuffle windows around Spaces; we refresh them - App.app.refreshOpenUi(Windows.list, .refreshUiAfterExternalEvent) - Logger.info { "screens:\(NSScreen.screens.map { ($0.uuid() ?? "nil" as CFString, $0.frame) })" } - Logger.info { "spaces:\(Spaces.screenSpacesMap)" } - Logger.info { "current space:\(Spaces.currentSpaceIndex) (id:\(Spaces.currentSpaceId))" } + // screen notifications often arrive in groups (e.g. 2 in a row in a short time) + // we debounce to minimize work + debouncerScreenAndSpace.debounce(.screenEvent) { + Logger.debug { notification.name.rawValue } + Spaces.refresh() + // a screen added or removed, or screen resolution change can mess up layout; we reset components + App.app.resetPreferencesDependentComponents() + } } } + +final class Debouncer { + private let queue: DispatchQueue + private var workItem: DispatchWorkItem? + private var contextsBlocks = [DebounceContext: () -> Void]() + + init(_ queue: DispatchQueue) { + self.queue = queue + } + + func debounce(_ context: DebounceContext, _ block: @escaping () -> Void) { + contextsBlocks[context] = block + workItem?.cancel() + let item = DispatchWorkItem(block: { + for block in self.contextsBlocks.values { + block() + } + // 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) + Logger.info { "screens:\(NSScreen.screens.map { ($0.uuid() ?? "nil" as CFString, $0.frame) })" } + Logger.info { "currentSpace:\(Spaces.currentSpaceIndex) (id:\(Spaces.currentSpaceId)) spaces:\(Spaces.screenSpacesMap)" } + }) + workItem = item + queue.asyncAfter(deadline: .now() + humanPerceptionDelay, execute: item) + } +} + +enum DebounceContext { + case spaceEvent + case screenEvent +} diff --git a/src/logic/events/SpacesEvents.swift b/src/logic/events/SpacesEvents.swift index fd43f50c..c4ed447a 100644 --- a/src/logic/events/SpacesEvents.swift +++ b/src/logic/events/SpacesEvents.swift @@ -6,14 +6,13 @@ class SpacesEvents { } @objc private static func handleEvent(_ notification: Notification) { - Logger.debug { notification.name.rawValue } - // if UI was kept open during Space transition, the Spaces may be obsolete; we refresh them - App.app.refreshOpenUi(Windows.list, .refreshUiAfterExternalEvent) - Logger.info { "current space:\(Spaces.currentSpaceIndex) (id:\(Spaces.currentSpaceId))" } - // Workaround for Safari full-screen videos - // when full-screening a video, Safari spawns a second full-screen window called "Safari" - // this window doesn't emit resize/move events. It doesn't pass isActualWindow on creation. It's added on focusedWindowChanged - // for such cases, we refresh isFullscreen on Space change - Windows.updateIsFullscreenOnCurrentSpace() + ScreensEvents.debouncerScreenAndSpace.debounce(.spaceEvent) { + Logger.debug { notification.name.rawValue } + // Workaround for Safari full-screen videos + // when full-screening a video, Safari spawns a second full-screen window called "Safari" + // this window doesn't emit resize/move events. It doesn't pass isActualWindow on creation. It's added on focusedWindowChanged + // for such cases, we refresh isFullscreen on Space change + Windows.updateIsFullscreenOnCurrentSpace() + } } }