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>
131 lines
4.4 KiB
Swift
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)
|
|
}()
|
|
}
|