Files
onyx/Packages/OnyxKit/Sources/NetworkProvider/NetworkMenuBarModule.swift
Scarriffle c44bb3af30 Menüleiste springt nicht mehr, Mixer fasst Hilfsprozesse zusammen
Die Breite folgte der Stellenzahl des aktuellen Werts. Aus „14 KB/s" wird
„431 KB/s", und mit der zusätzlichen Stelle wanderte die halbe Leiste. Jetzt
wieder feste Breiten, bemessen am breitesten Wert, der vorkommen kann.

Der frühere Fehler war nicht die feste Breite, sondern der falsche
Bezugswert: gezeichnet wird „63°", bemessen wurde „100 °C" — fünfzehn Punkte
für zwei Zeichen, die nie erscheinen. Nachgemessen: zwei Aufnahmen im
Abstand von sechs Sekunden mit geänderten Werten ergeben identische Breiten.

Der Mixer fasst Hilfsprozesse mit ihrem Programm zusammen. „Google Chrome"
und „Google Chrome Helper" standen als zwei Zeilen nebeneinander, mit
getrennten Reglern für dasselbe Programm — zieht man den einen, hört man
den anderen. Chrome hat je Tab einen Helfer; fünfzehn Regler für einen
Browser sind keine Auskunft.

Abgegriffen wird weiterhin je Prozess, geregelt aber je Programm. Der
Schlüssel der Taps muss deshalb den Prozess mitführen: mehrere Helfer teilen
sich eine Kennung und überschrieben sich sonst gegenseitig. Der Pegel einer
Zeile ist der lauteste ihrer Prozesse.

Die Zeilen sind einzeilig statt zweizeilig: Symbol, Stummschalter, Regler,
Wert. Der Name ist weg — bei einem erkennbaren Programmsymbol ist er
redundant, und die gewonnene Zeile macht die Liste halb so hoch. Dafür ist
das Symbol größer, und wer den Namen braucht, hält den Zeiger darauf.
Popover 420 statt 330 Punkte breit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 13:15:53 +02:00

200 lines
7.8 KiB
Swift

import AppKit
import SwiftUI
import OnyxMenuBar
import MetricsProvider
/// Das Netzwerk in der Menüleiste.
///
/// Anders als die Hardwaregrößen hat es **zwei** Werte, die beide interessieren.
/// Sie untereinander zu setzen ist der einzige Weg, beide in der Höhe einer
/// Menüleiste unterzubringen nebeneinander bräuchte es die doppelte Breite,
/// und die hat man dort nicht.
@MainActor
public final class NetworkMenuBarModule: MenuBarModule {
public let id = "network"
public var displayName: String { String(localized: "network.name", bundle: .module) }
private let model: NetworkModel
private var token: UUID?
private var view: NetworkStatusView?
public init(model: NetworkModel) { self.model = model }
public var shortLabel: String { String(localized: "menubar.short.network", bundle: .module) }
public func makeStatusView(presentation: MenuBarPresentation) -> NSView {
let view = NetworkStatusView(presentation: presentation)
view.update(model.snapshot, history: model.series(download: true))
self.view = view
return view
}
public func makePopoverView() -> AnyView {
AnyView(NetworkPopoverContent(model: model))
}
public func activate() {
guard token == nil else { return }
token = model.addConsumer(interval: 2)
startObserving()
}
public func deactivate() {
if let token { model.removeConsumer(token) }
token = nil
view = nil
}
private func startObserving() {
withObservationTracking {
_ = model.snapshot
} onChange: {
Task { @MainActor [weak self] in
guard let self, token != nil else { return }
view?.update(model.snapshot, history: model.series(download: true))
startObserving()
}
}
}
}
final class NetworkStatusView: NSView, WidthReporting {
private let presentation: MenuBarPresentation
private var snapshot = NetworkSnapshot()
private var history: [Double] = []
init(presentation: MenuBarPresentation) {
self.presentation = presentation
super.init(frame: NSRect(x: 0, y: 0, width: MenuBarText.minimumWidth, height: 22))
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
/// Breiter als die Hardwaremodule: 2,4 MB/s" braucht schlicht mehr Platz
/// als 46 %".
/// Der Platz für die **aktuellen** Raten.
///
/// Auf 999,9 MB/s" zu dimensionieren hieße, neben 13 KB/s" dauerhaft
/// die halbe Breite leer zu lassen und genau so sah es aus. Gemessen wird
/// deshalb, was gerade dasteht; `AdaptiveWidth` sorgt dafür, dass die
/// Leiste dabei nicht zappelt.
/// Feste Breite, bemessen am breitesten Wert, der vorkommen kann.
///
/// Nicht am aktuellen: aus 14 KB/s" wird 431 KB/s", und mit jeder
/// zusätzlichen Stelle wanderte die halbe Menüleiste. 999 MB/s" kostet
/// gegenüber einem dreistelligen KB-Wert nur wenige Punkte das ist der
/// Preis dafür, dass nie etwas springt.
private static var ratesWidth: CGFloat {
let font = NSFont.monospacedDigitSystemFont(ofSize: 9, weight: .regular)
let widest = NSAttributedString(string: "↓ 999 MB/s", attributes: [.font: font])
return ceil(widest.size().width) + 2 * MenuBarText.sidePadding
}
private func neededWidth() -> CGFloat {
switch presentation {
case .symbol:
max(MenuBarText.minimumWidth,
MenuBarText.symbolWidth(snapshot.interfaceKind.symbolName, height: 15) + 4)
case .graph, .bars: 28
case .value: Self.ratesWidth
case .valueAndGraph: Self.ratesWidth + 26
// Die Raten tragen mit und ihre Beschriftung schon in sich; ein
// Kürzel davor wäre doppelt gemoppelt, ein Symbol dagegen nützlich
// es zeigt WLAN, Ethernet oder VPN.
case .labelAndValue, .symbolAndValue:
Self.ratesWidth + symbolSlot + MenuBarText.innerGap
}
}
private var symbolSlot: CGFloat {
MenuBarText.symbolWidth(snapshot.interfaceKind.symbolName, height: 13)
}
private var width: CGFloat = MenuBarText.minimumWidth
var onWidthChange: ((CGFloat) -> Void)?
override var intrinsicContentSize: NSSize {
NSSize(width: width, height: 22)
}
func update(_ snapshot: NetworkSnapshot, history: [Double]) {
self.snapshot = snapshot
self.history = history
needsDisplay = true
let next = max(neededWidth(), MenuBarText.minimumWidth)
guard next != width else { return }
width = next
invalidateIntrinsicContentSize()
onWidthChange?(next)
}
override func draw(_ dirtyRect: NSRect) {
let color = NSColor.labelColor
switch presentation {
case .symbol:
MenuBarText.drawSymbol(snapshot.interfaceKind.symbolName, color: color,
in: NSRect(x: bounds.midX - 7.5, y: bounds.midY - 7.5,
width: 15, height: 15))
case .graph, .bars:
drawGraph(color: color, in: bounds.insetBy(dx: 2, dy: 5))
case .labelAndValue, .symbolAndValue:
let glyph = symbolSlot
MenuBarText.drawSymbol(snapshot.interfaceKind.symbolName, color: color,
in: NSRect(x: 0, y: bounds.midY - 6.5,
width: glyph, height: 13))
drawRates(color: color,
in: NSRect(x: glyph + MenuBarText.innerGap, y: 0,
width: bounds.width - glyph - MenuBarText.innerGap,
height: bounds.height))
case .value, .valueAndGraph:
drawRates(color: color,
in: presentation == .value ? bounds
: NSRect(x: 0, y: 0, width: bounds.width - 26, height: bounds.height))
if presentation == .valueAndGraph {
drawGraph(color: color,
in: NSRect(x: bounds.width - 24, y: 5, width: 22, height: 12))
}
}
}
/// Zwei Zeilen à 9 pt die einzige Art, Hoch und Runter in 22 pt Höhe
/// unterzubringen.
private func drawRates(color: NSColor, in rect: NSRect) {
let attributes: [NSAttributedString.Key: Any] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: 9, weight: .regular),
.foregroundColor: color,
]
let down = NSAttributedString(string: "" + Throughput.formatted(snapshot.downloadRate),
attributes: attributes)
let up = NSAttributedString(string: "" + Throughput.formatted(snapshot.uploadRate),
attributes: attributes)
// Mittig statt links: bleibt doch einmal Platz übrig etwa weil die
// zweite Zeile schmaler ist , verteilt er sich auf beide Seiten statt
// als Lücke rechts zu stehen.
down.draw(at: NSPoint(x: rect.midX - down.size().width / 2, y: rect.midY - 0.5))
up.draw(at: NSPoint(x: rect.midX - up.size().width / 2, y: rect.midY - 10.5))
}
private func drawGraph(color: NSColor, in rect: NSRect) {
guard history.count > 1, let context = NSGraphicsContext.current?.cgContext else { return }
let step = rect.width / CGFloat(history.count - 1)
context.setStrokeColor(color.withAlphaComponent(0.85).cgColor)
context.setLineWidth(1)
for (index, value) in history.enumerated() {
let point = CGPoint(x: rect.minX + CGFloat(index) * step,
y: rect.minY + rect.height * CGFloat(min(max(value, 0), 1)))
index == 0 ? context.move(to: point) : context.addLine(to: point)
}
context.strokePath()
}
}