11import AppKit
2- import Bonsplit
32import Foundation
43import SwiftUI
54
@@ -13,36 +12,19 @@ struct UpdatePill: View {
1312 var body : some View {
1413 if model. showsPill {
1514 pillButton
16- . popover (
17- isPresented: $showPopover,
18- attachmentAnchor: . rect( . bounds) ,
19- arrowEdge: . top
20- ) {
21- UpdatePopoverView ( model: model)
15+ . background ( UpdatePillPopoverAnchor ( isPresented: $showPopover, model: model) )
16+ . onChange ( of: model. showsPill) { _, showsPill in
17+ if !showsPill {
18+ showPopover = false
19+ }
2220 }
2321 . transition ( . opacity. combined ( with: . scale( scale: 0.95 ) ) )
2422 }
2523 }
2624
2725 @ViewBuilder
2826 private var pillButton : some View {
29- Button ( action: {
30- if model. showsDetectedBackgroundUpdate {
31- if showPopover {
32- showPopover = false
33- } else {
34- showPopover = true
35- AppDelegate . shared? . checkForUpdatesInCustomUI ( )
36- }
37- return
38- }
39- if case . notFound( let notFound) = model. state {
40- model. state = . idle
41- notFound. acknowledgement ( )
42- } else {
43- showPopover. toggle ( )
44- }
45- } ) {
27+ Button ( action: handleTap) {
4628 HStack ( spacing: 6 ) {
4729 UpdateBadge ( model: model)
4830 . frame ( width: 14 , height: 14 )
@@ -68,13 +50,141 @@ struct UpdatePill: View {
6850 . accessibilityIdentifier ( " UpdatePill " )
6951 }
7052
53+ private func handleTap( ) {
54+ if model. showsDetectedBackgroundUpdate {
55+ if model. hasCachedDetectedUpdateDetails {
56+ showPopover. toggle ( )
57+ } else if showPopover {
58+ showPopover = false
59+ } else {
60+ showPopover = true
61+ AppDelegate . shared? . checkForUpdatesInCustomUI ( )
62+ }
63+ return
64+ }
65+
66+ if case . notFound( let notFound) = model. state {
67+ model. state = . idle
68+ notFound. acknowledgement ( )
69+ } else {
70+ showPopover. toggle ( )
71+ }
72+ }
73+
7174 private var textWidth : CGFloat ? {
7275 let attributes : [ NSAttributedString . Key : Any ] = [ . font: textFont]
7376 let size = ( model. maxWidthText as NSString ) . size ( withAttributes: attributes)
7477 return size. width
7578 }
7679}
7780
81+ private struct UpdatePillPopoverAnchor : NSViewRepresentable {
82+ @Binding var isPresented : Bool
83+ @ObservedObject var model : UpdateViewModel
84+
85+ func makeNSView( context: Context ) -> NSView {
86+ let view = NSView ( )
87+ context. coordinator. anchorView = view
88+ return view
89+ }
90+
91+ func updateNSView( _ nsView: NSView , context: Context ) {
92+ let coordinator = context. coordinator
93+ context. coordinator. anchorView = nsView
94+ context. coordinator. updateRootView (
95+ AnyView (
96+ UpdatePopoverView ( model: model) {
97+ [ weak coordinator] in
98+ coordinator? . closeFromContent ( )
99+ }
100+ )
101+ )
102+
103+ if isPresented {
104+ context. coordinator. present ( )
105+ } else {
106+ context. coordinator. dismiss ( )
107+ }
108+ }
109+
110+ func makeCoordinator( ) -> Coordinator {
111+ Coordinator ( isPresented: $isPresented)
112+ }
113+
114+ static func dismantleNSView( _ nsView: NSView , coordinator: Coordinator ) {
115+ coordinator. dismiss ( )
116+ }
117+
118+ final class Coordinator : NSObject , NSPopoverDelegate {
119+ @Binding var isPresented : Bool
120+
121+ weak var anchorView : NSView ?
122+ private let hostingController = NSHostingController ( rootView: AnyView ( EmptyView ( ) ) )
123+ private var popover : NSPopover ?
124+
125+ init ( isPresented: Binding < Bool > ) {
126+ _isPresented = isPresented
127+ }
128+
129+ func updateRootView( _ rootView: AnyView ) {
130+ hostingController. rootView = rootView
131+ hostingController. view. invalidateIntrinsicContentSize ( )
132+ hostingController. view. layoutSubtreeIfNeeded ( )
133+ updateContentSize ( )
134+ }
135+
136+ func present( ) {
137+ guard let anchorView, anchorView. window != nil else {
138+ isPresented = false
139+ dismiss ( )
140+ return
141+ }
142+
143+ anchorView. superview? . layoutSubtreeIfNeeded ( )
144+ let popover = popover ?? makePopover ( )
145+ updateContentSize ( )
146+ guard !popover. isShown else { return }
147+
148+ popover. show ( relativeTo: anchorView. bounds, of: anchorView, preferredEdge: . maxY)
149+ }
150+
151+ func dismiss( ) {
152+ popover? . performClose ( nil )
153+ }
154+
155+ func closeFromContent( ) {
156+ isPresented = false
157+ dismiss ( )
158+ }
159+
160+ func popoverDidClose( _ notification: Notification ) {
161+ popover = nil
162+ if isPresented {
163+ isPresented = false
164+ }
165+ }
166+
167+ private func makePopover( ) -> NSPopover {
168+ let popover = NSPopover ( )
169+ popover. behavior = . semitransient
170+ popover. animates = true
171+ popover. contentViewController = hostingController
172+ popover. delegate = self
173+ self . popover = popover
174+ return popover
175+ }
176+
177+ private func updateContentSize( ) {
178+ let fittingSize = hostingController. view. fittingSize
179+ guard fittingSize. width > 0 , fittingSize. height > 0 else { return }
180+ popover? . contentSize = NSSize (
181+ width: ceil ( fittingSize. width) ,
182+ height: ceil ( fittingSize. height)
183+ )
184+ }
185+ }
186+ }
187+
78188/// Menu item that shows "Install Update and Relaunch" when an update is ready.
79189struct InstallUpdateMenuItem : View {
80190 @ObservedObject var model : UpdateViewModel
0 commit comments