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