Ausgabe und Eingabe stehen jetzt über den Programmen — mit Lautstärke, Stummschalter und der Gerätewahl direkt am Namen. Ein eigenes Auswahlfeld daneben wäre eine zweite Stelle für dieselbe Sache. Sie stehen **außerhalb** des Mixer-Schalters, und das ist der Punkt: hier wird kein Ton abgegriffen, sondern es werden die Regler des Systems bedient. Das braucht keinen Tap, kein Aggregate-Gerät und keine Berechtigung — wer nur die Lautstärke sucht, soll dafür nichts einschalten müssen. Zwei Dinge, die beim Bauen nicht offensichtlich waren: Viele Geräte führen keinen Regler für Element 0, sondern nur je Kanal. Fragt man bloß den Hauptregler, hält man ein regelbares Gerät für unregelbar. Also erst der Hauptregler, dann die Kanäle — und beim Setzen beide Kanäle, sonst wandert das Stereobild. Manche Geräte bringen gar keinen Regler mit, HDMI etwa. Dort steht „hier nicht regelbar" statt eines Reglers, der nichts tut. Warntöne folgen dem Ausgabegerät mit. Zwei getrennte Wahlen sind die Quelle für „warum kommt der Systemton aus dem Notebook". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
114 lines
3.6 KiB
Swift
114 lines
3.6 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 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) {
|
|
// 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))
|
|
}
|
|
}
|