Der Regler war eine Pseudobedienung. Der SMC lehnt Schreibzugriffe auf CHLT ab — der Helfer hat es mit korrekter Größe versucht und „geschrieben (false), zurückgelesen 85" gemeldet. Das Sicherheitsnetz hat also die Wahrheit gesagt; die Oberfläche hat sie nur nicht gezeigt. Weiter zu raten hieße, im Lade-Subsystem Keys durchzuprobieren, und das ist die eine Operation in diesem Projekt, die Hardware dauerhaft beschädigen kann. Also: das Limit wird gelesen und angezeigt, mit einem Weg in die Systemeinstellungen, wo es tatsächlich geht. Der Schreibpfad ist aus dem privilegierten Helfer entfernt — ein Root-Dienst trägt nur, was benutzt wird. Die Akku-Kachel zeigte „40 %" und einen Verlauf über die letzten Sekunden. Der Ladestand ändert sich in Minuten, nicht in Sekunden; der Graph war eine waagerechte Linie. Jetzt: verbleibende Zeit bzw. Zeit bis voll, Leistung in Watt mit Richtung, Ladelimit, Zustand, Zyklen — und der Energiesparmodus. Der erklärt, warum die Maschine langsamer wirkt, und ohne diesen Hinweis sucht man den Grund woanders. In der Menüleiste standen fünf nackte Prozentzahlen nebeneinander. Man sah Zahlen und wusste nicht, welche wozu gehört. Zwei neue Darstellungen: Kürzel und Wert („CPU 34 %") sowie Symbol und Wert. Beim Akku stehen sie zuoberst in der Auswahl. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
108 lines
3.4 KiB
Swift
108 lines
3.4 KiB
Swift
import SwiftUI
|
|
import AppKit
|
|
import AudioProvider
|
|
import OnyxMenuBar
|
|
import OnyxDesign
|
|
|
|
/// Der Lautstärkemixer als eigenes Element in der Menüleiste.
|
|
///
|
|
/// Bewusst getrennt vom Onyx-Element: den Ton einer einzelnen App leiser zu
|
|
/// drehen ist etwas, das man mitten in etwas anderem tut. Ein Umweg über ein
|
|
/// Menü und dann in ein Panel ist dafür zwei Schritte zu weit.
|
|
@MainActor
|
|
final class MixerMenuBarModule: MenuBarModule {
|
|
let id = "audio"
|
|
var displayName: String { String(localized: "mixer.menubar.name") }
|
|
|
|
var shortLabel: String { String(localized: "mixer.menubar.short") }
|
|
|
|
private let mixer: AudioMixer
|
|
private var view: MixerStatusView?
|
|
private var isActive = false
|
|
|
|
init(mixer: AudioMixer) { self.mixer = mixer }
|
|
|
|
func makeStatusView(presentation: MenuBarPresentation) -> NSView {
|
|
let view = MixerStatusView()
|
|
view.update(isEnabled: mixer.isEnabled, managed: mixer.managed.count)
|
|
self.view = view
|
|
return view
|
|
}
|
|
|
|
func makePopoverView() -> AnyView {
|
|
AnyView(
|
|
ScrollView {
|
|
MixerView(mixer: mixer)
|
|
.padding(14)
|
|
}
|
|
.scrollIndicators(.automatic)
|
|
.frame(width: 280)
|
|
.frame(maxHeight: 420)
|
|
)
|
|
}
|
|
|
|
func activate() {
|
|
isActive = true
|
|
startObserving()
|
|
}
|
|
|
|
func deactivate() {
|
|
isActive = false
|
|
view = nil
|
|
}
|
|
|
|
/// Der Mixer misst ohnehin; hier wird nur zugehört. `withObservationTracking`
|
|
/// meldet sich einmal und muss danach neu eingerichtet werden.
|
|
private func startObserving() {
|
|
withObservationTracking {
|
|
_ = mixer.isEnabled
|
|
_ = mixer.managed
|
|
} onChange: {
|
|
Task { @MainActor [weak self] in
|
|
guard let self, isActive else { return }
|
|
view?.update(isEnabled: mixer.isEnabled, managed: mixer.managed.count)
|
|
startObserving()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Ein Glyph, mehr nicht.
|
|
///
|
|
/// Eine Zahl wäre hier sinnlos: „welche Lautstärke" hat bei einem Mischpult
|
|
/// keine einzelne Antwort. Der Zustand, der zählt, ist ob überhaupt etwas
|
|
/// geregelt wird — und den zeigt die Deckkraft.
|
|
private final class MixerStatusView: NSView {
|
|
private var isEnabled = false
|
|
private var managed = 0
|
|
|
|
init() {
|
|
super.init(frame: NSRect(x: 0, y: 0, width: MenuBarText.minimumWidth, height: 22))
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) { fatalError() }
|
|
|
|
override var intrinsicContentSize: NSSize {
|
|
NSSize(width: MenuBarText.minimumWidth, height: 22)
|
|
}
|
|
|
|
func update(isEnabled: Bool, managed: Int) {
|
|
self.isEnabled = isEnabled
|
|
self.managed = managed
|
|
needsDisplay = true
|
|
}
|
|
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
// Ausgeschaltet bleibt es sichtbar, aber zurückgenommen: das Element
|
|
// verschwinden zu lassen wäre die schlechtere Antwort — dann fände man
|
|
// den Weg zum Einschalten nicht mehr.
|
|
let color = (isEnabled && managed > 0)
|
|
? NSColor.labelColor
|
|
: NSColor.labelColor.withAlphaComponent(0.55)
|
|
MenuBarText.drawSymbol("slider.horizontal.3", color: color,
|
|
in: NSRect(x: bounds.midX - 7.5, y: bounds.midY - 7.5,
|
|
width: 15, height: 15))
|
|
}
|
|
}
|