import AppKit import SwiftUI @MainActor class MenuController: NSObject { static private(set) weak var shared: MenuController? let statusItem: NSStatusItem let popover = NSPopover() var eventMonitor: EventMonitor? init(statusItem: NSStatusItem) { self.statusItem = statusItem super.init() if let button = statusItem.button { button.action = #selector(togglePopover(_:)) button.target = self } // Explicitly set content size to match MainView's frame popover.contentSize = NSSize(width: 450, height: 640) popover.behavior = .transient popover.contentViewController = NSHostingController(rootView: MainView()) eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in if let self = self, self.popover.isShown { self.closePopover(sender: event) } } MenuController.shared = self } /// Keep the popover open while a modal window (e.g. an NSOpenPanel) is up. /// A transient popover otherwise dismisses itself the moment another window /// takes focus, which closed the folder picker before it could be used. func beginModalPresentation() { eventMonitor?.stop() popover.behavior = .applicationDefined } func endModalPresentation() { popover.behavior = .transient if popover.isShown { eventMonitor?.start() } } @objc func togglePopover(_ sender: AnyObject?) { if popover.isShown { closePopover(sender: sender) } else { showPopover(sender: sender) } } func showPopover(sender: AnyObject?) { if let button = statusItem.button { popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) eventMonitor?.start() } } func closePopover(sender: AnyObject?) { popover.performClose(sender) eventMonitor?.stop() } } class EventMonitor { private var monitor: Any? private let mask: NSEvent.EventTypeMask private let handler: (NSEvent?) -> Void init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) { self.mask = mask self.handler = handler } deinit { stop() } func start() { monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) } func stop() { if let monitor = monitor { NSEvent.removeMonitor(monitor) self.monitor = nil } } }