From 63dd7281f5597b6b8aa187d78f1dc53b2b3e6d63 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Mon, 30 Mar 2026 01:43:41 -0700 Subject: [PATCH] Fix fullscreen new windows opening in current Space (#2345) * Fix fullscreen new windows opening in current Space * works * Stabilize fullscreen tiling regression test --- Sources/AppDelegate.swift | 28 +++++++++- .../AppDelegateShortcutRoutingTests.swift | 55 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 9698bc7d..fe445861 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -2147,6 +2147,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent let isFirstResponder: Bool } var debugCloseMainWindowConfirmationHandler: ((NSWindow) -> Bool)? + var debugCreateMainWindowSourceIsNativeFullScreenOverride: Bool? // Keep debug-only windows alive when tests intentionally inject key mismatches. private var debugDetachedContextWindows: [NSWindow] = [] @@ -5975,9 +5976,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent // Use the current key window's size for new windows so Cmd+Shift+N // creates a window matching the previous one's dimensions. let styleMask: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView] - let existingFrame = preferredMainWindowContextForWorkspaceCreation( + let sourceContext = preferredMainWindowContextForWorkspaceCreation( debugSource: "createMainWindow.initialGeometry" - ).flatMap { resolvedWindow(for: $0)?.frame } + ) + let sourceWindow = sourceContext.flatMap { resolvedWindow(for: $0) } + let existingFrame = sourceWindow?.frame + let sourceWindowIsNativeFullScreen: Bool = { +#if DEBUG + if let debugCreateMainWindowSourceIsNativeFullScreenOverride { + return debugCreateMainWindowSourceIsNativeFullScreenOverride + } +#endif + return sourceWindow?.styleMask.contains(.fullScreen) == true + }() + let shouldTemporarilyDisallowFullScreenTiling = + sessionWindowSnapshot == nil && sourceWindowIsNativeFullScreen let initialRect: NSRect if sessionWindowSnapshot == nil, let existingFrame { // Convert frame rect to content rect so the new window matches the @@ -5993,6 +6006,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent backing: .buffered, defer: false ) + // When creating a new window from an existing native fullscreen window, + // temporarily opt out of fullscreen tiling so AppKit doesn't place the + // new window into the active fullscreen Space. + if shouldTemporarilyDisallowFullScreenTiling { + window.collectionBehavior.insert(.fullScreenDisallowsTiling) + } window.title = "" window.titleVisibility = .hidden window.titlebarAppearsTransparent = true @@ -6045,6 +6064,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent setActiveMainWindow(window) NSApp.activate(ignoringOtherApps: true) } + if shouldTemporarilyDisallowFullScreenTiling { + DispatchQueue.main.async { [weak window] in + window?.collectionBehavior.remove(.fullScreenDisallowsTiling) + } + } if let restoredFrame { window.setFrame(restoredFrame, display: true) #if DEBUG diff --git a/cmuxTests/AppDelegateShortcutRoutingTests.swift b/cmuxTests/AppDelegateShortcutRoutingTests.swift index a759b098..b3245cf7 100644 --- a/cmuxTests/AppDelegateShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -33,6 +33,7 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { override func tearDown() { AppDelegate.shared?.shortcutLayoutCharacterProvider = KeyboardLayout.character(forKeyCode:modifierFlags:) AppDelegate.shared?.debugCloseMainWindowConfirmationHandler = nil + AppDelegate.shared?.debugCreateMainWindowSourceIsNativeFullScreenOverride = nil AppDelegate.shared?.dismissNotificationsPopoverIfShown() RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05)) for action in KeyboardShortcutSettings.Action.allCases { @@ -98,6 +99,60 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { XCTAssertEqual(secondManager.tabs.count, secondCount + 1, "Cmd+N should add workspace to the event's window") } + func testCreateMainWindowDoesNotDisallowFullScreenTilingByDefault() { + guard let appDelegate = AppDelegate.shared else { + XCTFail("Expected AppDelegate.shared") + return + } + + let windowId = appDelegate.createMainWindow() + defer { + closeWindow(withId: windowId) + } + + guard let window = window(withId: windowId) else { + XCTFail("Expected test window") + return + } + + XCTAssertFalse( + window.collectionBehavior.contains(.fullScreenDisallowsTiling), + "Main windows should still support standard macOS Split View when not created from a fullscreen source" + ) + } + + func testCreateMainWindowTemporarilyDisallowsFullScreenTilingFromFullscreenSource() { + guard let appDelegate = AppDelegate.shared else { + XCTFail("Expected AppDelegate.shared") + return + } + + appDelegate.debugCreateMainWindowSourceIsNativeFullScreenOverride = true + + let newWindowId = appDelegate.createMainWindow() + defer { + closeWindow(withId: newWindowId) + } + + guard let newWindow = window(withId: newWindowId) else { + XCTFail("Expected new window") + return + } + + XCTAssertTrue( + newWindow.collectionBehavior.contains(.fullScreenDisallowsTiling), + "New windows should temporarily opt out of fullscreen tiling while opening from a fullscreen source" + ) + + appDelegate.debugCreateMainWindowSourceIsNativeFullScreenOverride = nil + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05)) + + XCTAssertFalse( + newWindow.collectionBehavior.contains(.fullScreenDisallowsTiling), + "The fullscreen tiling opt-out should be cleared after initial presentation so Split View keeps working" + ) + } + func testAddWorkspaceInPreferredMainWindowIgnoresStaleTabManagerPointer() { guard let appDelegate = AppDelegate.shared else { XCTFail("Expected AppDelegate.shared")