@@ -29,11 +29,13 @@ actor WidgetWindowsController: NSObject {
2929 var lastUpdateWindowLocationTime = Date ( timeIntervalSince1970: 0 )
3030
3131 var beatingCompletionPanelTask : Task < Void , Error > ?
32+ var updateWindowStateTask : Task < Void , Error > ?
3233
3334 deinit {
3435 userDefaultsObservers. presentationModeChangeObserver. onChange = { }
3536 observeToAppTask? . cancel ( )
3637 observeToFocusedEditorTask? . cancel ( )
38+ updateWindowStateTask? . cancel ( )
3739 }
3840
3941 init ( store: StoreOf < WidgetFeature > , chatTabPool: ChatTabPool ) {
@@ -73,6 +75,23 @@ actor WidgetWindowsController: NSObject {
7375 await self ? . send ( . updateColorScheme)
7476 }
7577 }
78+
79+ updateWindowStateTask = Task { [ weak self] in
80+ if let self { await handleXcodeFullscreenChange ( ) }
81+
82+ await withThrowingTaskGroup ( of: Void . self) { [ weak self] group in
83+ // active space did change
84+ _ = group. addTaskUnlessCancelled { [ weak self] in
85+ let sequence = NSWorkspace . shared. notificationCenter
86+ . notifications ( named: NSWorkspace . activeSpaceDidChangeNotification)
87+ for await _ in sequence {
88+ guard let self else { return }
89+ try Task . checkCancellation ( )
90+ await handleXcodeFullscreenChange ( )
91+ }
92+ }
93+ }
94+ }
7695 }
7796}
7897
@@ -135,7 +154,11 @@ private extension WidgetWindowsController {
135154 }
136155
137156 switch notification. kind {
138- case . focusedWindowChanged, . focusedUIElementChanged:
157+ case . focusedWindowChanged:
158+ await handleXcodeFullscreenChange ( )
159+ await hideWidgetForTransitions ( )
160+ await updateWidgetsAndNotifyChangeOfEditor ( immediately: true )
161+ case . focusedUIElementChanged:
139162 await hideWidgetForTransitions ( )
140163 await updateWidgetsAndNotifyChangeOfEditor ( immediately: true )
141164 case . applicationActivated:
@@ -547,6 +570,35 @@ extension WidgetWindowsController {
547570 window. setFloatOnTop ( overlap)
548571 }
549572 }
573+
574+ @MainActor
575+ func handleXcodeFullscreenChange( ) async {
576+ guard let activeXcode = await XcodeInspector . shared. safe. activeXcode
577+ else { return }
578+
579+ let xcode = activeXcode. appElement
580+ let isFullscreen = if let xcodeWindow = xcode. focusedWindow {
581+ xcodeWindow. isFullScreen && xcode. isFrontmost
582+ } else {
583+ false
584+ }
585+
586+ [
587+ windows. chatPanelWindow,
588+ windows. sharedPanelWindow,
589+ windows. suggestionPanelWindow,
590+ windows. widgetWindow,
591+ windows. toastWindow,
592+ ] . forEach {
593+ $0. send ( . didChangeActiveSpace( fullscreen: isFullscreen) )
594+ }
595+
596+ if windows. fullscreenDetector. isOnActiveSpace {
597+ if xcode. focusedWindow != nil {
598+ windows. orderFront ( )
599+ }
600+ }
601+ }
550602}
551603
552604// MARK: - NSWindowDelegate
@@ -623,7 +675,7 @@ public final class WidgetWindows {
623675
624676 @MainActor
625677 lazy var widgetWindow = {
626- let it = CanBecomeKeyWindow (
678+ let it = WidgetWindow (
627679 contentRect: . zero,
628680 styleMask: . borderless,
629681 backing: . buffered,
@@ -632,8 +684,7 @@ public final class WidgetWindows {
632684 it. isReleasedWhenClosed = false
633685 it. isOpaque = false
634686 it. backgroundColor = . clear
635- it. level = . floating
636- it. collectionBehavior = [ . fullScreenAuxiliary, . transient, . canJoinAllSpaces]
687+ it. level = widgetLevel ( 0 )
637688 it. hasShadow = true
638689 it. contentView = NSHostingView (
639690 rootView: WidgetView (
@@ -650,7 +701,7 @@ public final class WidgetWindows {
650701
651702 @MainActor
652703 lazy var sharedPanelWindow = {
653- let it = CanBecomeKeyWindow (
704+ let it = WidgetWindow (
654705 contentRect: . init( x: 0 , y: 0 , width: Style . panelWidth, height: Style . panelHeight) ,
655706 styleMask: . borderless,
656707 backing: . buffered,
@@ -660,7 +711,6 @@ public final class WidgetWindows {
660711 it. isOpaque = false
661712 it. backgroundColor = . clear
662713 it. level = widgetLevel ( 2 )
663- it. collectionBehavior = [ . fullScreenAuxiliary, . transient, . canJoinAllSpaces]
664714 it. hasShadow = true
665715 it. contentView = NSHostingView (
666716 rootView: SharedPanelView (
@@ -684,7 +734,7 @@ public final class WidgetWindows {
684734
685735 @MainActor
686736 lazy var suggestionPanelWindow = {
687- let it = CanBecomeKeyWindow (
737+ let it = WidgetWindow (
688738 contentRect: . init( x: 0 , y: 0 , width: Style . panelWidth, height: Style . panelHeight) ,
689739 styleMask: . borderless,
690740 backing: . buffered,
@@ -694,7 +744,6 @@ public final class WidgetWindows {
694744 it. isOpaque = false
695745 it. backgroundColor = . clear
696746 it. level = widgetLevel ( 2 )
697- it. collectionBehavior = [ . fullScreenAuxiliary, . transient, . canJoinAllSpaces]
698747 it. hasShadow = true
699748 it. contentView = NSHostingView (
700749 rootView: SuggestionPanelView (
@@ -730,7 +779,7 @@ public final class WidgetWindows {
730779
731780 @MainActor
732781 lazy var toastWindow = {
733- let it = CanBecomeKeyWindow (
782+ let it = WidgetWindow (
734783 contentRect: . zero,
735784 styleMask: [ . borderless] ,
736785 backing: . buffered,
@@ -740,7 +789,6 @@ public final class WidgetWindows {
740789 it. isOpaque = true
741790 it. backgroundColor = . clear
742791 it. level = widgetLevel ( 0 )
743- it. collectionBehavior = [ . fullScreenAuxiliary, . transient, . canJoinAllSpaces]
744792 it. hasShadow = false
745793 it. contentView = NSHostingView (
746794 rootView: ToastPanelView ( store: store. scope (
@@ -782,9 +830,58 @@ class CanBecomeKeyWindow: NSWindow {
782830 override var canBecomeMain : Bool { canBecomeKeyChecker ( ) }
783831}
784832
833+ class WidgetWindow : CanBecomeKeyWindow {
834+ enum State : Equatable {
835+ case normal( fullscreen: Bool )
836+ case switchingSpace
837+ }
838+
839+ enum Action {
840+ case didChangeActiveSpace( fullscreen: Bool )
841+ }
842+
843+ var defaultCollectionBehavior : NSWindow . CollectionBehavior {
844+ [ . fullScreenAuxiliary, . transient]
845+ }
846+
847+ var fullscreenCollectionBehavior : NSWindow . CollectionBehavior {
848+ // .canJoinAllSpaces is required for macOS 15 (beta?) to display widgets in fullscreen mode.
849+ // But adding this behavior will create another issue that the widgets will display
850+ // whenever user switch spaces, so we are setting it only when the window is in fullscreen
851+ // mode.
852+ [ . fullScreenAuxiliary, . transient, . canJoinAllSpaces]
853+ }
854+
855+ var switchingSpaceCollectionBehavior : NSWindow . CollectionBehavior {
856+ [ . fullScreenAuxiliary, . transient]
857+ }
858+
859+ private var state : State ? {
860+ didSet {
861+ guard state != oldValue else { return }
862+ switch state {
863+ case . none:
864+ collectionBehavior = defaultCollectionBehavior
865+ case . switchingSpace:
866+ collectionBehavior = switchingSpaceCollectionBehavior
867+ case let . normal( fullscreen) :
868+ collectionBehavior = fullscreen
869+ ? fullscreenCollectionBehavior
870+ : defaultCollectionBehavior
871+ }
872+ }
873+ }
874+
875+ func send( _ action: Action ) {
876+ switch action {
877+ case let . didChangeActiveSpace( fullscreen) :
878+ state = . normal( fullscreen: fullscreen)
879+ }
880+ }
881+ }
882+
785883func widgetLevel( _ addition: Int ) -> NSWindow . Level {
786884 let minimumWidgetLevel : Int
787885 minimumWidgetLevel = NSWindow . Level. floating. rawValue
788886 return . init( minimumWidgetLevel + addition)
789887}
790-
0 commit comments