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 let system: SystemAudio private var view: MixerStatusView? private var isActive = false init(mixer: AudioMixer, system: SystemAudio) { self.mixer = mixer self.system = system } 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, system: system) .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) { // Volle Deckkraft, auch ausgeschaltet. // // Die Abdunklung war meine Idee und ein Fehlgriff: in der Menüleiste // liegt oft ein heller Schreibtischhintergrund, und ein zu 55 % blasses // Glyph ist dort praktisch weg. Ob der Mixer läuft, erfährt man beim // Klick — dafür muss man das Symbol aber erst finden. MenuBarText.drawSymbol("slider.horizontal.3", color: .labelColor, in: NSRect(x: bounds.midX - 7.5, y: bounds.midY - 7.5, width: 15, height: 15)) } }