Files
UTM/Platform/macOS/Display/VMDisplayWindowController.swift
T
osy 73f875c915 toolbar(macOS): draw VM power controls as one segmented control
macOS 26 draws a single Liquid Glass background behind a run of adjacent
toolbar items, so stop, start/pause, and restart read as one undivided
button. A space item only splits that background into separate groups.
The divider Safari's back/forward and Xcode's run/stop show comes from a
segmented control inside one toolbar item, so the nib now defines the
three controls as segments of one.

Clicking the stop segment still stops the VM, and clicking and holding
it still shows the shutdown menu, which now hangs off the segment rather
than an NSMenuToolbarItem. Enabling moves to the individual segments.
Segment tooltips are localizable, so the existing translations move from
the removed items to the segments. An autosaved toolbar item list that
names the removed items falls back to the nib default, so existing
installs pick up the new control.

Segments have no label, so VoiceOver reads their image descriptions.
The stop and restart images get localized descriptions the way the USB
item's image does, and start/pause already had them. German lacked a
"Restart" translation, so the one the removed toolbar item carried moves
to Localizable.strings, where the main menu's Restart item uses it too.
The group's "Power" label reuses the existing Chinese translations.

Assisted-by: Claude:claude-opus-5
2026-09-18 07:10:45 +02:00

671 lines
27 KiB
Swift

//
// Copyright © 2020 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import IOKit.pwr_mgt
class VMDisplayWindowController: NSWindowController, UTMVirtualMachineDelegate {
enum Control {
case power
case startPause
case restart
case captureInput
case usb
case drives
case sharedFolder
case resize
case windows
case keyboardShortcut
}
@IBOutlet weak var displayView: NSView!
@IBOutlet weak var screenshotView: NSImageView!
@IBOutlet private weak var overlayView: NSVisualEffectView!
@IBOutlet private weak var activityIndicator: NSProgressIndicator!
@IBOutlet private weak var startButton: NSButton!
@IBOutlet private weak var toolbar: NSToolbar!
@IBOutlet private weak var powerSegmentedControl: NSSegmentedControl!
@IBOutlet private weak var captureMouseToolbarItem: NSToolbarItem!
@IBOutlet weak var captureMouseToolbarButton: NSButton!
@IBOutlet private weak var usbToolbarItem: NSToolbarItem!
@IBOutlet private weak var drivesToolbarItem: NSToolbarItem!
@IBOutlet private weak var sharedFolderToolbarItem: NSToolbarItem!
@IBOutlet private weak var resizeConsoleToolbarItem: NSToolbarItem!
@IBOutlet private weak var windowsToolbarItem: NSToolbarItem!
@IBOutlet private weak var keyboardShortcutsItem: NSToolbarItem!
/// Segments of the power control, in the order the toolbar draws them.
private enum PowerSegment: Int {
case stop
case startPause
case restart
}
private var mainMenu: NSMenu!
private var mainMenuItem: NSMenuItem!
private var stopMenuItem: NSMenuItem!
private var startPauseMenuItem: NSMenuItem!
private var restartMenuItem: NSMenuItem!
private var usbMenuItem: NSMenuItem!
private var drivesMenuItem: NSMenuItem!
private var sharedFolderMenuItem: NSMenuItem!
private var windowsMenuItem: NSMenuItem!
private var keyboardShortcutMenuItem: NSMenuItem!
var shouldAutoStartVM: Bool = true
var vm: (any UTMVirtualMachine)!
var onClose: (() -> Void)?
private(set) var secondaryWindows: [VMDisplayWindowController] = []
private(set) weak var primaryWindow: VMDisplayWindowController?
private var preventIdleSleepAssertion: IOPMAssertionID?
private var hasSaveSnapshotFailed: Bool = false
private var isFinalizing: Bool = false
@Setting("PreventIdleSleep") private var isPreventIdleSleep: Bool = false
@Setting("NoQuitConfirmation") private var isNoQuitConfirmation: Bool = false
var isSecondary: Bool {
primaryWindow != nil
}
override var windowNibName: NSNib.Name? {
"VMDisplayWindow"
}
override weak var owner: AnyObject? {
self
}
@objc dynamic func updateUsbMenu(_ menu: NSMenu) {}
@objc dynamic func updateDrivesMenu(_ menu: NSMenu) {}
@objc dynamic func updateSharedFolderMenu(_ menu: NSMenu) {}
@objc dynamic func updateWindowsMenu(_ menu: NSMenu) {}
@objc dynamic func updateKeyboardShortcutMenu(_ menu: NSMenu) {}
convenience init(vm: any UTMVirtualMachine, onClose: (() -> Void)?) {
self.init(window: nil)
self.vm = vm
self.onClose = onClose
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWake), name: NSWorkspace.didWakeNotification, object: nil)
}
deinit {
NSWorkspace.shared.notificationCenter.removeObserver(self, name: NSWorkspace.didWakeNotification, object: nil)
}
private func stop(isKill: Bool = false) {
showConfirmAlert(NSLocalizedString("This may corrupt the VM and any unsaved changes will be lost. To quit safely, shut down from the guest.", comment: "VMDisplayWindowController")) {
self.enterSuspended(isBusy: true) // early indicator
if self.vm.registryEntry.isSuspended {
self.vm.requestVmDeleteState()
}
self.vm.requestVmStop(force: isKill)
}
}
@IBAction func stopButtonPressed(_ sender: Any) {
if vm.state == .started || vm.state == .paused {
stop(isKill: false)
} else {
stop(isKill: true)
}
}
@IBAction func startPauseButtonPressed(_ sender: Any) {
enterSuspended(isBusy: true) // early indicator
if vm.state == .started {
vm.requestVmPause()
} else if vm.state == .paused {
vm.requestVmResume()
} else if vm.state == .stopped {
vm.requestVmStart()
} else {
logger.error("Invalid state \(vm.state)")
}
}
@IBAction func restartButtonPressed(_ sender: Any) {
showConfirmAlert(NSLocalizedString("This will reset the VM and any unsaved state will be lost.", comment: "VMDisplayWindowController")) {
self.vm.requestVmReset()
}
}
@IBAction private func powerSegmentPressed(_ sender: NSSegmentedControl) {
switch PowerSegment(rawValue: sender.selectedSegment) {
case .stop:
stopButtonPressed(sender)
case .startPause:
startPauseButtonPressed(sender)
case .restart:
restartButtonPressed(sender)
case nil:
break
}
}
@IBAction dynamic func captureMouseButtonPressed(_ sender: Any) {
}
@IBAction dynamic func resizeConsoleButtonPressed(_ sender: Any) {
}
@IBAction dynamic func usbButtonPressed(_ sender: Any) {
let menu = NSMenu()
updateUsbMenu(menu)
menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
}
@IBAction dynamic func drivesButtonPressed(_ sender: Any) {
let menu = NSMenu()
updateDrivesMenu(menu)
menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
}
@IBAction dynamic func sharedFolderButtonPressed(_ sender: Any) {
let menu = NSMenu()
updateSharedFolderMenu(menu)
menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
}
@IBAction dynamic func windowsButtonPressed(_ sender: Any) {
let menu = NSMenu()
updateWindowsMenu(menu)
menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
}
@IBAction dynamic func keyboardShortcutsButtonPressed(_ sender: Any) {
let menu = NSMenu()
updateKeyboardShortcutMenu(menu)
menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
}
@MainActor
func setControl(_ controls: [Control], isEnabled: Bool) {
for control in controls {
switch control {
case .power:
powerSegmentedControl.setEnabled(isEnabled, forSegment: PowerSegment.stop.rawValue)
stopMenuItem.isEnabled = isEnabled
case .startPause:
powerSegmentedControl.setEnabled(isEnabled, forSegment: PowerSegment.startPause.rawValue)
startPauseMenuItem.isEnabled = isEnabled
case .restart:
powerSegmentedControl.setEnabled(isEnabled, forSegment: PowerSegment.restart.rawValue)
restartMenuItem.isEnabled = isEnabled
case .captureInput:
captureMouseToolbarItem.isEnabled = isEnabled
case .usb:
usbToolbarItem.isEnabled = isEnabled
usbMenuItem.isEnabled = isEnabled
case .drives:
drivesToolbarItem.isEnabled = isEnabled
drivesMenuItem.isEnabled = isEnabled
case .sharedFolder:
sharedFolderToolbarItem.isEnabled = isEnabled
sharedFolderMenuItem.isEnabled = isEnabled
case .resize:
resizeConsoleToolbarItem.isEnabled = isEnabled
case .windows:
windowsToolbarItem.isEnabled = isEnabled
case .keyboardShortcut:
keyboardShortcutsItem.isEnabled = isEnabled
keyboardShortcutMenuItem.isEnabled = isEnabled
}
}
}
@MainActor
func setControl(_ control: Control, isEnabled: Bool) {
setControl([control], isEnabled: isEnabled)
}
// MARK: - UI states
override func windowDidLoad() {
window!.recalculateKeyViewLoop()
setupStopButtonMenu()
setupMainMenu()
setupToolbarIcons()
if vm.state == .stopped {
enterSuspended(isBusy: false)
} else {
enterLive()
}
super.windowDidLoad()
}
private func setupToolbarIcons() {
// cable.connector ships in SF Symbols 3, so the USB toolbar item
// visually matches the surrounding system symbols.
usbToolbarItem.image = NSImage(systemSymbolName: "cable.connector",
accessibilityDescription: usbToolbarItem.label)
// segments have no label for VoiceOver to read, only their image
let stopDescription = NSLocalizedString("Stop", comment: "VMDisplayWindowController")
powerSegmentedControl.setImage(NSImage(systemSymbolName: "power", accessibilityDescription: stopDescription),
forSegment: PowerSegment.stop.rawValue)
let restartDescription = NSLocalizedString("Restart", comment: "VMDisplayWindowController")
powerSegmentedControl.setImage(NSImage(systemSymbolName: "restart", accessibilityDescription: restartDescription),
forSegment: PowerSegment.restart.rawValue)
}
public func requestAutoStart(options: UTMVirtualMachineStartOptions = []) {
guard shouldAutoStartVM else {
return
}
DispatchQueue.global(qos: .userInitiated).async {
if (self.vm.state == .stopped) {
self.vm.requestVmStart(options: options)
} else if (self.vm.state == .paused) {
self.vm.requestVmResume()
}
}
}
func enterLive() {
overlayView.isHidden = true
activityIndicator.stopAnimation(self)
let pauseDescription = NSLocalizedString("Pause", comment: "VMDisplayWindowController")
powerSegmentedControl.setImage(NSImage(systemSymbolName: "pause", accessibilityDescription: pauseDescription),
forSegment: PowerSegment.startPause.rawValue)
startPauseMenuItem.title = pauseDescription
setControl([.startPause, .power, .restart, .captureInput, .resize, .windows, .keyboardShortcut], isEnabled: true)
window!.makeFirstResponder(displayView.subviews.first)
if isPreventIdleSleep && !isSecondary {
var preventIdleSleepAssertion: IOPMAssertionID = .zero
let success = IOPMAssertionCreateWithName(kIOPMAssertPreventUserIdleSystemSleep as CFString,
IOPMAssertionLevel(kIOPMAssertionLevelOn),
"UTM Virtual Machine Running" as CFString,
&preventIdleSleepAssertion)
if success == kIOReturnSuccess {
self.preventIdleSleepAssertion = preventIdleSleepAssertion
}
}
}
func enterSuspended(isBusy busy: Bool) {
overlayView.isHidden = false
let playDescription = NSLocalizedString("Start", comment: "VMDisplayWindowController")
let stopped = vm.state == .stopped
powerSegmentedControl.setImage(NSImage(systemSymbolName: "play.fill", accessibilityDescription: playDescription),
forSegment: PowerSegment.startPause.rawValue)
startPauseMenuItem.title = playDescription
if busy {
activityIndicator.startAnimation(self)
setControl([.startPause, .power, .restart], isEnabled: false)
startButton.isHidden = true
} else {
activityIndicator.stopAnimation(self)
setControl(.startPause, isEnabled: true)
startButton.isHidden = false
setControl([.power, .restart], isEnabled: !stopped)
}
setControl([.captureInput, .resize, .drives, .sharedFolder, .usb, .windows, .keyboardShortcut], isEnabled: false)
window!.makeFirstResponder(nil)
if let preventIdleSleepAssertion = preventIdleSleepAssertion {
IOPMAssertionRelease(preventIdleSleepAssertion)
}
}
// MARK: - Alert
@MainActor
func showErrorAlert(_ message: String, completionHandler handler: ((NSApplication.ModalResponse) -> Void)? = nil) {
window?.resignKey()
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = NSLocalizedString("Error", comment: "VMDisplayWindowController")
alert.informativeText = message
alert.beginSheetModal(for: window!, completionHandler: handler)
}
@MainActor
func showConfirmAlert(_ message: String, confirmHandler handler: (() -> Void)? = nil) {
window?.resignKey()
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = NSLocalizedString("Confirmation", comment: "VMDisplayWindowController")
alert.informativeText = message
alert.addButton(withTitle: NSLocalizedString("OK", comment: "VMDisplayWindowController"))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "VMDisplayWindowController"))
alert.beginSheetModal(for: window!) { response in
if response == .alertFirstButtonReturn {
handler?()
}
}
}
@nonobjc nonisolated func withErrorAlert(_ callback: @escaping () async throws -> Void) {
Task.detached(priority: .background) { [self] in
do {
try await callback()
} catch {
Task { @MainActor in
showErrorAlert(error.localizedDescription)
}
}
}
}
// MARK: - Create a secondary window
func registerSecondaryWindow(_ secondaryWindow: VMDisplayWindowController, at index: Int? = nil) {
secondaryWindows.insert(secondaryWindow, at: index ?? secondaryWindows.endIndex)
secondaryWindow.onClose = { [weak self] in
self?.secondaryWindows.removeAll(where: { $0 == secondaryWindow })
}
secondaryWindow.primaryWindow = self
secondaryWindow.showWindow(self)
self.showWindow(self) // show primary window on top
secondaryWindow.virtualMachine(vm, didTransitionToState: vm.state) // show correct starting state
}
// MARK: - Virtual machine delegate
func virtualMachine(_ vm: any UTMVirtualMachine, didTransitionToState state: UTMVirtualMachineState) {
Task { @MainActor in
guard !isFinalizing else {
return
}
switch state {
case .stopped, .paused:
enterSuspended(isBusy: false)
case .pausing, .stopping, .starting, .resuming, .saving, .restoring:
enterSuspended(isBusy: true)
case .started:
enterLive()
}
for subwindow in secondaryWindows {
subwindow.virtualMachine(vm, didTransitionToState: state)
}
}
}
func virtualMachine(_ vm: any UTMVirtualMachine, didErrorWithMessage message: String) {
Task { @MainActor in
guard !isFinalizing else {
return
}
showErrorAlert(message) { _ in
if vm.state != .started && vm.state != .paused {
self.close()
}
}
}
}
func virtualMachine(_ vm: any UTMVirtualMachine, didCompleteInstallation success: Bool) {
}
func virtualMachine(_ vm: any UTMVirtualMachine, didUpdateInstallationProgress progress: Double) {
}
}
extension VMDisplayWindowController: NSWindowDelegate {
func window(_ window: NSWindow, willUseFullScreenPresentationOptions proposedOptions: NSApplication.PresentationOptions = []) -> NSApplication.PresentationOptions {
return proposedOptions.union([.autoHideToolbar])
}
func windowShouldClose(_ sender: NSWindow) -> Bool {
guard !isSecondary else {
return true
}
guard !(vm.state == .stopped || (vm.state == .paused && vm.registryEntry.isSuspended)) else {
return true
}
if let snapshotUnsupportedError = vm.snapshotUnsupportedError {
return windowWillCloseAfterConfirmation(sender, error: snapshotUnsupportedError)
} else if hasSaveSnapshotFailed {
return windowWillCloseAfterConfirmation(sender)
} else {
return windowWillCloseAfterSaving(sender)
}
}
private func windowWillCloseAfterConfirmation(_ sender: NSWindow, error: Error? = nil) -> Bool {
guard !isNoQuitConfirmation else {
return true
}
let alert = NSAlert()
alert.alertStyle = .informational
if error == nil {
alert.messageText = NSLocalizedString("Confirmation", comment: "VMDisplayWindowController")
} else {
alert.messageText = NSLocalizedString("Failed to save suspend state", comment: "VMDisplayWindowController")
}
alert.informativeText = NSLocalizedString("Closing this window will kill the VM.", comment: "VMQemuDisplayMetalWindowController")
if let error = error {
alert.informativeText = error.localizedDescription + "\n" + alert.informativeText
}
alert.addButton(withTitle: NSLocalizedString("OK", comment: "VMDisplayWindowController"))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "VMDisplayWindowController"))
alert.showsSuppressionButton = true
alert.beginSheetModal(for: sender) { response in
switch response {
case .alertFirstButtonReturn:
if alert.suppressionButton?.state == .on {
self.isNoQuitConfirmation = true
}
sender.close()
default:
return
}
}
return false
}
private func windowWillCloseAfterSaving(_ sender: NSWindow) -> Bool {
Task {
do {
try await vm.saveSnapshot(name: nil)
vm.delegate = nil
self.enterSuspended(isBusy: false)
sender.close()
} catch {
hasSaveSnapshotFailed = true
_ = windowWillCloseAfterConfirmation(sender, error: error)
}
}
return false
}
func windowWillClose(_ notification: Notification) {
if !isSecondary {
self.vm.requestVmStop(force: true)
}
secondaryWindows.forEach { secondaryWindow in
secondaryWindow.close()
}
if let preventIdleSleepAssertion = preventIdleSleepAssertion {
IOPMAssertionRelease(preventIdleSleepAssertion)
}
cleanupMenu()
isFinalizing = true
onClose?()
}
func windowDidBecomeKey(_ notification: Notification) {
if let window = self.window {
_ = window.makeFirstResponder(displayView.subviews.first)
}
}
func windowDidResignKey(_ notification: Notification) {
if let window = self.window {
_ = window.makeFirstResponder(nil)
}
}
}
// MARK: - Toolbar
extension VMDisplayWindowController: NSToolbarItemValidation {
func validateToolbarItem(_ item: NSToolbarItem) -> Bool {
return true
}
}
// MARK: - Stop menu
extension VMDisplayWindowController {
private func updateStopButtonMenu(_ menu: NSMenu) {
menu.autoenablesItems = false
let item1 = NSMenuItem()
item1.title = NSLocalizedString("Request power down", comment: "VMDisplayWindowController")
item1.toolTip = NSLocalizedString("Sends power down request to the guest. This simulates pressing the power button on a PC.", comment: "VMDisplayWindowController")
item1.target = self
item1.action = #selector(requestPowerDown)
menu.addItem(item1)
let item2 = NSMenuItem()
item2.title = NSLocalizedString("Force shut down", comment: "VMDisplayWindowController")
item2.toolTip = NSLocalizedString("Tells the VM process to shut down with risk of data corruption. This simulates holding down the power button on a PC.", comment: "VMDisplayWindowController")
item2.target = self
item2.action = #selector(forceShutDown)
menu.addItem(item2)
if type(of: vm).capabilities.supportsProcessKill {
let item3 = NSMenuItem()
item3.title = NSLocalizedString("Force kill", comment: "VMDisplayWindowController")
item3.toolTip = NSLocalizedString("Force kill the VM process with high risk of data corruption.", comment: "VMDisplayWindowController")
item3.target = self
item3.action = #selector(forceKill)
menu.addItem(item3)
}
}
private func setupStopButtonMenu() {
let menu = NSMenu()
updateStopButtonMenu(menu)
// shown when the segment is clicked and held
powerSegmentedControl.setMenu(menu, forSegment: PowerSegment.stop.rawValue)
}
@MainActor @objc private func requestPowerDown(sender: AnyObject) {
vm.requestGuestPowerDown()
}
@MainActor @objc private func forceShutDown(sender: AnyObject) {
stop()
}
@MainActor @objc private func forceKill(sender: AnyObject) {
stop(isKill: true)
}
}
// MARK: - Computer wakeup
extension VMDisplayWindowController {
@objc func didWake(_ notification: NSNotification) {
// do something in subclass
}
}
// MARK: - Main menu
extension VMDisplayWindowController {
@discardableResult
@objc func setupMainMenu() -> NSMenu {
NotificationCenter.default.addObserver(self, selector: #selector(windowBecameMain), name: NSWindow.didBecomeMainNotification, object: window)
NotificationCenter.default.addObserver(self, selector: #selector(windowResignedMain), name: NSWindow.didResignMainNotification, object: window)
NotificationCenter.default.addObserver(self, selector: #selector(menuDidRemoveItem), name: NSMenu.didRemoveItemNotification, object: NSApp.mainMenu)
let menu = NSMenu()
menu.autoenablesItems = false
stopMenuItem = NSMenuItem()
stopMenuItem.title = NSLocalizedString("Power", comment: "VMDisplayWindowController")
let stopMenu = NSMenu()
updateStopButtonMenu(stopMenu)
stopMenuItem.submenu = stopMenu
menu.addItem(stopMenuItem)
startPauseMenuItem = NSMenuItem(title: "", action: #selector(startPauseButtonPressed), keyEquivalent: "")
menu.addItem(startPauseMenuItem)
restartMenuItem = NSMenuItem(title: NSLocalizedString("Restart", comment: "VMDisplayWindowController"), action: #selector(restartButtonPressed), keyEquivalent: "")
menu.addItem(restartMenuItem)
menu.addItem(.separator())
keyboardShortcutMenuItem = LazyMenuItem { [weak self] in self?.updateKeyboardShortcutMenu($0) }
keyboardShortcutMenuItem.title = NSLocalizedString("Send Key", comment: "VMDisplayWindowController")
menu.addItem(keyboardShortcutMenuItem)
menu.addItem(.separator())
usbMenuItem = LazyMenuItem { [weak self] in self?.updateUsbMenu($0) }
usbMenuItem.title = NSLocalizedString("USB Devices", comment: "VMDisplayWindowController")
menu.addItem(usbMenuItem)
drivesMenuItem = LazyMenuItem { [weak self] in self?.updateDrivesMenu($0) }
drivesMenuItem.title = NSLocalizedString("Drives", comment: "VMDisplayWindowController")
menu.addItem(drivesMenuItem)
sharedFolderMenuItem = LazyMenuItem { [weak self] in self?.updateSharedFolderMenu($0) }
sharedFolderMenuItem.title = NSLocalizedString("Shared Folder", comment: "VMDisplayWindowController")
menu.addItem(sharedFolderMenuItem)
windowsMenuItem = LazyMenuItem { [weak self] in self?.updateWindowsMenu($0) }
windowsMenuItem.title = NSLocalizedString("Displays", comment: "VMDisplayWindowController")
menu.addItem(windowsMenuItem)
mainMenu = menu
mainMenuItem = NSMenuItem()
mainMenuItem.title = NSLocalizedString("Virtual Machine", comment: "VMDisplayWindowController")
mainMenuItem.submenu = menu
return menu
}
func cleanupMenu() {
NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeMainNotification, object: window)
NotificationCenter.default.removeObserver(self, name: NSWindow.didResignMainNotification, object: window)
NotificationCenter.default.removeObserver(self, name: NSMenu.didRemoveItemNotification, object: NSApp.mainMenu)
if let mainMenu = NSApp.mainMenu, mainMenu.items.contains(mainMenuItem!) {
mainMenu.removeItem(mainMenuItem)
}
}
@objc func windowBecameMain(_ notification: Notification) {
if let mainMenu = NSApp.mainMenu, !mainMenu.items.contains(mainMenuItem) {
mainMenu.insertItem(mainMenuItem, at: 3)
}
}
@objc func windowResignedMain(_ notification: Notification) {
if let mainMenu = NSApp.mainMenu, mainMenu.items.contains(mainMenuItem) {
mainMenu.removeItem(mainMenuItem)
}
}
@objc func menuDidRemoveItem(_ notification: Notification) {
guard let window = window, window.isMainWindow else {
return
}
if let mainMenu = NSApp.mainMenu, !mainMenu.items.contains(mainMenuItem) {
mainMenu.insertItem(mainMenuItem, at: 3)
}
}
}
private class LazyMenuItem: NSMenuItem, NSMenuDelegate {
private var menuUpdate: (NSMenu) -> Void
init(menuUpdate: @escaping (NSMenu) -> Void) {
self.menuUpdate = menuUpdate
super.init(title: "", action: nil, keyEquivalent: "")
let menu = NSMenu()
menu.delegate = self
self.submenu = menu
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func menuWillOpen(_ menu: NSMenu) {
menu.removeAllItems()
menuUpdate(menu)
}
}