diff --git a/DuckDuckGo/Common/Extensions/NSWorkspaceExtension.swift b/DuckDuckGo/Common/Extensions/NSWorkspaceExtension.swift index ebfca54a36..15bc5f3a68 100644 --- a/DuckDuckGo/Common/Extensions/NSWorkspaceExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSWorkspaceExtension.swift @@ -29,33 +29,32 @@ extension NSWorkspace { return bundle.displayName } + /// Detect if macOS Mission Control (three-finger swipe up to show the Spaces) is currently active static func isMissionControlActive() -> Bool { guard let visibleWindows = CGWindowListCopyWindowInfo(.optionOnScreenOnly, CGWindowID(0)) as? [[CFString: Any]] else { assertionFailure("CGWindowListCopyWindowInfo doesn‘t work anymore") return false } - let allScreenSizes = NSScreen.screens.map(\.frame.size) - // Here‘s the trick: normally the Dock App only displays full-screen overlay windows drawing the Dock. // When the Mission Control is activated, the Dock presents multiple window tiles for each visible window // so here we filter out all the screen-sized windows and if the resulting list is not empty it may // mean that Mission Control is active. - let missionControlWindows = visibleWindows.filter { window in - windowName(window) == "Dock" && !allScreenSizes.contains(windowSize(window)) + let dockAppWindows = visibleWindows.filter { window in + window.ownerName == "Dock" } - - func windowName(_ dict: [CFString: Any]) -> String? { - dict[kCGWindowOwnerName] as? String + // filter out wallpaper windows + var missionControlWindows = dockAppWindows.filter { window in + window.name?.hasPrefix("Wallpaper") != true } - func windowSize(_ dict: [CFString: Any]) -> NSSize { - guard let bounds = dict[kCGWindowBounds] as? [String: NSNumber], - let width = bounds["Width"]?.intValue, - let height = bounds["Height"]?.intValue else { return .zero } - return NSSize(width: width, height: height) + // filter out the Dock drawing windows + for screen in NSScreen.screens { + if let idx = missionControlWindows.firstIndex(where: { window in window.size == screen.frame.size }) { + missionControlWindows.remove(at: idx) + } } - return missionControlWindows.count > allScreenSizes.count + return missionControlWindows.count > 0 } @available(macOS, obsoleted: 14.0, message: "This needs to be removed as it‘s no longer necessary.") @@ -79,3 +78,22 @@ extension NSWorkspace.OpenConfiguration { } } + +private extension [CFString: Any] { + + var name: String? { + self[kCGWindowName] as? String + } + + var ownerName: String? { + self[kCGWindowOwnerName] as? String + } + + var size: NSSize { + guard let bounds = self[kCGWindowBounds] as? [String: NSNumber], + let width = bounds["Width"]?.intValue, + let height = bounds["Height"]?.intValue else { return .zero } + return NSSize(width: width, height: height) + } + +} diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 1fd8c17493..a6fce323b1 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -333,36 +333,18 @@ protocol NewWindowPolicyDecisionMaker { } deinit { - cleanUpBeforeClosing(onDeinit: true, webView: webView, userContentController: userContentController) - } - - func cleanUpBeforeClosing() { - cleanUpBeforeClosing(onDeinit: false, webView: webView, userContentController: userContentController) - } - - @MainActor(unsafe) - private func cleanUpBeforeClosing(onDeinit: Bool, webView: WebView, userContentController: UserContentController?) { - let job = { [webView, userContentController] in + DispatchQueue.main.asyncOrNow { [webView, userContentController] in + // WebKit objects must be deallocated on the main thread webView.stopAllMedia(shouldStopLoading: true) userContentController?.cleanUpBeforeClosing() + #if DEBUG if case .normal = NSApp.runType { webView.assertObjectDeallocated(after: 4.0) } #endif } -#if DEBUG - if !onDeinit, case .normal = NSApp.runType { - // Tab should be deallocated shortly after burning - self.assertObjectDeallocated(after: 4.0) - } -#endif - guard Thread.isMainThread else { - DispatchQueue.main.async { job() } - return - } - job() } func stopAllMediaAndLoading() { diff --git a/DuckDuckGo/Tab/View/WebViewContainerView.swift b/DuckDuckGo/Tab/View/WebViewContainerView.swift index e75c81c627..722144ed57 100644 --- a/DuckDuckGo/Tab/View/WebViewContainerView.swift +++ b/DuckDuckGo/Tab/View/WebViewContainerView.swift @@ -51,7 +51,6 @@ final class WebViewContainerView: NSView { } private var blurViewIsHiddenCancellable: AnyCancellable? - private var fullScreenWindowWillCloseCancellable: AnyCancellable? private var cancellables = Set() override func didAddSubview(_ subview: NSView) { @@ -117,48 +116,37 @@ final class WebViewContainerView: NSView { .store(in: &cancellables) } - // fix a glitch scaling down Full Screen layer on next Full Screen activation - // after exiting Full Screen by dragging the window out in Mission Control - // (three-fingers-up swipe) - // see https://app.asana.com/0/1177771139624306/1204370242122745/f - private func observeFullScreenWindowWillExitFullScreen(_ fullScreenWindow: NSWindow) { - NotificationCenter.default.publisher(for: NSWindow.willExitFullScreenNotification, object: fullScreenWindow) - .sink { [weak self] _ in - guard let self else { return } - self.cancellables.removeAll() - - if NSWorkspace.isMissionControlActive() { - // closeAllMediaPresentations causes all Full Screen windows to be closed and removed from their WebViews - // (and reinstantiated the next time Full Screen is requested) - // this would slightly break UX in case multiple Full Screen windows are open but it fixes the bug - if #available(macOS 12.0, *) { - webView.closeAllMediaPresentations {} - } else { - webView.closeAllMediaPresentations() - } + /** - } - } - .store(in: &cancellables) + Fix a glitch breaking the Full Screen presentation on a repeated + Full Screen mode activation after dragging out of Mission Control Spaces. - // https://app.asana.com/0/72649045549333/1206959015087322/f - if #unavailable(macOS 14.4) { - fullScreenWindowWillCloseCancellable = NotificationCenter.default.publisher(for: NSWindow.willCloseNotification, object: fullScreenWindow) - .sink { [weak self] notification in - self?.fullScreenWindowWillCloseCancellable = nil - let fullScreenWindowController = (notification.object as? NSWindow)?.windowController - DispatchQueue.main.async { [weak fullScreenWindowController] in - guard let fullScreenWindowController else { return } - // just in case. - // if WKFullScreenWindowController receives `close()` the next time it‘s open it will crash because its _webView is nil - // https://errors.duckduckgo.com/organizations/ddg/issues/3411/?project=6&referrer=release-issue-stream - NSException.try { - fullScreenWindowController.setValue(NSView(), forKeyPath: #keyPath(webView)) - } + **Steps to reproduce:** + 1. Enter full screen video + 2. Open Mission Control (swipe three fingers up) + 3. Drag the full screen video out of the top panel in the Mission Control + 4. Enter full screen again - validate video opens in full screen + - The video would open in a shrinked (thumbnail) state without the fix + + - Note: The bug is actual for macOS 12 and above + + https://app.asana.com/0/1177771139624306/1204370242122745/f + */ + private func observeFullScreenWindowWillExitFullScreen(_ fullScreenWindow: NSWindow) { + if #available(macOS 12.0, *) { // works fine on Big Sur + NotificationCenter.default.publisher(for: NSWindow.willExitFullScreenNotification, object: fullScreenWindow) + .sink { [weak self] _ in + guard let self else { return } + self.cancellables.removeAll() + + if NSWorkspace.isMissionControlActive() { + // closeAllMediaPresentations causes all Full Screen windows to be closed and removed from their WebViews + // (and reinstantiated the next time Full Screen is requested) + webView.closeAllMediaPresentations {} } } + .store(in: &cancellables) } - } override func removeFromSuperview() {