MenuController.swift

2.4 KB · raw

1import AppKit2import SwiftUI34@MainActor5class MenuController: NSObject {6  static private(set) weak var shared: MenuController?78  let statusItem: NSStatusItem9  let popover: NSPopover10  var eventMonitor: EventMonitor?1112  init(statusItem: NSStatusItem, popover: NSPopover? = nil) {13    self.statusItem = statusItem14    self.popover = popover ?? NSPopover()15    super.init()1617    if let button = statusItem.button {18      button.action = #selector(togglePopover(_:))19      button.target = self20    }2122    // Explicitly set content size to match MainView's frame23    self.popover.contentSize = NSSize(width: 450, height: 640)24    self.popover.behavior = .transient25    self.popover.contentViewController = NSHostingController(rootView: MainView())2627    eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in28      if let self = self, self.popover.isShown {29        self.closePopover(sender: event)30      }31    }3233    MenuController.shared = self34  }3536  /// Keep the popover open while a modal window (e.g. an NSOpenPanel) is up.37  /// A transient popover otherwise dismisses itself the moment another window38  /// takes focus, which closed the folder picker before it could be used.39  func beginModalPresentation() {40    eventMonitor?.stop()41    popover.behavior = .applicationDefined42  }4344  func endModalPresentation() {45    popover.behavior = .transient46    if popover.isShown {47      eventMonitor?.start()48    }49  }5051  @objc func togglePopover(_ sender: AnyObject?) {52    if popover.isShown {53      closePopover(sender: sender)54    } else {55      showPopover(sender: sender)56    }57  }5859  func showPopover(sender: AnyObject?) {60    if let button = statusItem.button {61      popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)62      eventMonitor?.start()63    }64  }6566  func closePopover(sender: AnyObject?) {67    popover.performClose(sender)68    eventMonitor?.stop()69  }70}7172class EventMonitor {73  private var monitor: Any?74  private let mask: NSEvent.EventTypeMask75  let handler: (NSEvent?) -> Void7677  init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) {78    self.mask = mask79    self.handler = handler80  }8182  deinit {83    stop()84  }8586  func start() {87    monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler)88  }8990  func stop() {91    if let monitor = monitor {92      NSEvent.removeMonitor(monitor)93      self.monitor = nil94    }95  }96}