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) // Breiter als das Panel: hier ist der Mixer zu Hause, und eine // Zeile aus Symbol, Stummschalter, Regler und Wert braucht Platz. .frame(width: 330) .frame(maxHeight: 460) ) } 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)) } }