Files
onyx/Onyx/MixerMenuBarModule.swift
Scarriffle 43ce40676b Eigenes Symbol für den Mixer in der Menüleiste
Die Datei lag im Projektstamm; von dort wird nichts mitgeliefert. Jetzt in
Onyx/ und damit im Bundle.

Gezeichnet wird sie in `labelColor`, nicht in Weiß: unter einer hellen
Menüleiste wäre ein weißes Symbol unsichtbar. Auf dunklem Grund ist
labelColor weiß, also genau das Gewünschte — und auf hellem Grund schwarz,
statt zu verschwinden.

Eigene Vorlagen lassen sich nicht wie SF-Symbole einfärben: dort greift die
Symbolkonfiguration nicht, und `color.set()` allein färbt ein Template-Bild
ohnehin nicht. Der Weg ist, das Bild zu zeichnen und die Farbe mit
`sourceAtop` darüberzulegen — das trifft die deckenden Bildpunkte und lässt
den Rest frei.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 13:23:21 +02:00

131 lines
4.4 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.
let rect = NSRect(x: bounds.midX - 8, y: bounds.midY - 8, width: 16, height: 16)
if let glyph = Self.glyph {
// `labelColor` und nicht Weiß: unter einer hellen Menüleiste wäre
// ein weißes Symbol unsichtbar. Auf dunklem Grund ist labelColor
// weiß, also genau das Gewünschte.
MenuBarText.drawTemplate(glyph, color: .labelColor, in: rect)
return
}
MenuBarText.drawSymbol("slider.horizontal.3", color: .labelColor, in: rect)
}
/// Die eigene Vorlage aus dem Bundle. Einmal geladen und behalten das
/// Zeichnen läuft bei jeder Änderung, das Einlesen einer SVG nicht.
private static let glyph: NSImage? = {
guard let url = Bundle.main.url(forResource: "sound-waves", withExtension: "svg")
else { return nil }
return NSImage(contentsOf: url)
}()
}