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>
116 lines
3.8 KiB
Swift
116 lines
3.8 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(16)
|
|
}
|
|
.scrollIndicators(.automatic)
|
|
// Deutlich breiter als das Panel: hier ist der Mixer zu Hause.
|
|
// Eine Zeile aus Symbol, Stummschalter, Regler und Wert braucht
|
|
// Platz, und ein Regler von hundert Punkten Länge lässt sich nicht
|
|
// fein einstellen.
|
|
.frame(width: 420)
|
|
.frame(maxHeight: 620)
|
|
)
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|