Files
onyx/Onyx/MixerMenuBarModule.swift
Scarriffle eff3c30a71 Mixer im Zeilenlayout, Akku zeigt „am Netz, lädt nicht"
Am Netz und trotzdem kein Blitz sah aus wie Akkubetrieb — dabei ist es der
Normalfall, sobald das Ladelimit greift. Jetzt steht in diesem Zustand ein
Stecker im Akku, ausgestanzt wie der Blitz. Drei Zustände, drei Zeichen:
lädt, hängt am Netz, läuft auf Akku.

Der Mixer folgt jetzt der Anordnung von SoundSource, weil sie dort gut
gelöst ist: eine Zeile je Programm, links das Symbol als Kachel, dann der
Stummschalter, dann der Regler, rechts der Wert. Vorher stand der
Stummschalter rechts zwischen den Zusatzelementen, obwohl er zur Lautstärke
gehört, und die Prozentzahl sprang zwischen zwei Zeilen.

Die Farben folgen SoundSource **nicht**. Dort ist jeder Regler grün; in Onyx
ist Farbe ein Signal — die Verstärkung über 100 % ist eines, eine
gewöhnliche Lautstärke nicht.

Nebenbei zwei Fehler im Regler: der Knopf stand an den Enden über die Bahn
hinaus, und die Ziehposition rechnete ohne seine Breite. Am rechten Anschlag
ließen sich die letzten Prozent deshalb nicht einstellen.

Das Popover ist die Hauptfläche und jetzt 330 statt 280 Punkte breit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:05:37 +02:00

110 lines
3.5 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)
// 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))
}
}