100 lines
3.8 KiB
Swift
100 lines
3.8 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import AppKit
|
|
import SwiftUI
|
|
import TermindApplication
|
|
|
|
/// Owns the status item and popover independently of window and SwiftUI scene lifetimes.
|
|
@MainActor
|
|
final class MetricsMenuBarController: NSObject, NSPopoverDelegate {
|
|
private let defaults: UserDefaults
|
|
private var item: NSStatusItem?
|
|
private let popover: NSPopover
|
|
private var observer: (any NSObjectProtocol)?
|
|
private weak var model: ServerMetricsModel?
|
|
private var clearsInitialFocus = false
|
|
var openMainWindow: () -> Void = {}
|
|
var openDetails: (UUID) -> Void = { _ in }
|
|
|
|
init(defaults: UserDefaults = .standard, popover: NSPopover = NSPopover()) {
|
|
self.defaults = defaults
|
|
self.popover = popover
|
|
}
|
|
|
|
func start(_ model: ServerMetricsModel) {
|
|
self.model = model
|
|
if observer == nil {
|
|
observer = NotificationCenter.default.addObserver(
|
|
forName: UserDefaults.didChangeNotification, object: nil, queue: .main
|
|
) { [weak self] _ in
|
|
Task { @MainActor in self?.refresh() }
|
|
}
|
|
}
|
|
refresh()
|
|
}
|
|
|
|
func refresh() {
|
|
let enabled = defaults.object(forKey: DesktopSettingsKey.showMetricsInMenuBar) as? Bool ?? true
|
|
guard enabled else {
|
|
popover.close()
|
|
if let item { NSStatusBar.system.removeStatusItem(item) }
|
|
item = nil
|
|
return
|
|
}
|
|
guard item == nil, model != nil else { return }
|
|
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
|
|
let image = NSImage(named: "MenuBarIcon")?.copy() as? NSImage
|
|
image?.size = NSSize(width: 18, height: 18)
|
|
image?.isTemplate = true
|
|
item.button?.image = image
|
|
item.button?.toolTip = ApplicationIdentity.displayName
|
|
item.button?.setAccessibilityLabel(ApplicationIdentity.displayName)
|
|
item.button?.target = self
|
|
item.button?.action = #selector(togglePopover)
|
|
self.item = item
|
|
}
|
|
|
|
@objc func togglePopover() {
|
|
guard let model, let button = item?.button else { return }
|
|
if popover.isShown {
|
|
popover.close()
|
|
return
|
|
}
|
|
let event = NSApp.currentEvent
|
|
let keyboardOpening = (event?.type == .keyDown || event?.type == .keyUp) && event?.window === button.window
|
|
clearsInitialFocus = !keyboardOpening
|
|
if clearsInitialFocus { UserInputModalityTracker.shared.notePointerInteraction() }
|
|
let view = MetricsMenuView(model: model) { [weak self] in
|
|
self?.popover.close()
|
|
self?.openMainWindow()
|
|
} openDetails: { [weak self] hostID in
|
|
self?.popover.close()
|
|
self?.openDetails(hostID)
|
|
}
|
|
.environment(\.locale, AppLanguage.stored().locale)
|
|
popover.delegate = self
|
|
popover.behavior = .transient
|
|
popover.contentViewController = NSHostingController(rootView: view)
|
|
popover.contentSize = MetricsMenuView.preferredSize(serverCount: model.servers.count)
|
|
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
|
popover.contentViewController?.view.window?.makeKey()
|
|
}
|
|
|
|
func popoverDidShow(_ notification: Notification) {
|
|
guard clearsInitialFocus, UserInputModalityTracker.shared.modality == .pointer else { return }
|
|
let window = popover.contentViewController?.view.window
|
|
window?.initialFirstResponder = nil
|
|
window?.makeFirstResponder(nil)
|
|
}
|
|
|
|
func stop() {
|
|
popover.close()
|
|
if let item { NSStatusBar.system.removeStatusItem(item) }
|
|
item = nil
|
|
if let observer { NotificationCenter.default.removeObserver(observer) }
|
|
observer = nil
|
|
model = nil
|
|
}
|
|
}
|