Compare commits

...

3 Commits

Author SHA1 Message Date
Scarriffle
ee848b86cc Das Aufnahmesymbol kam vom Mixer, nicht vom Pegel
Gemessen über kAudioProcessPropertyIsRunningInput: mit laufendem Mixer nimmt
Onyx auf, ohne ihn nicht. Ein Process Tap ist für macOS eine Tonaufnahme —
auch wenn damit nur die Lautstärke geregelt wird. Solange der Mixer Chrome und
Firefox abgreift, steht das orange Symbol in der Leiste.

Der Mixer stand seit der Fehlersuche am Wochenende auf „automatisch starten",
deshalb war es dauerhaft. Das Häkchen steht jetzt wieder aus.

Ein Hinweis unter dem Häkchen sagt das jetzt, wo die Entscheidung fällt: wer
das Symbol ohne Erklärung sieht, sucht den Fehler bei sich oder hält die App
für unsauber.

Dazu ein echter Fund an derselben Stelle: der Mikrofonpegel hing an
`onDisappear` der View — und das bleibt bei einem NSPopover aus. Dessen
Controller lebt nach dem Schließen weiter, die View verschwindet nie aus der
Hierarchie. Die Messung wäre also beim ersten Öffnen angegangen und nie wieder
aus. Sie hängt jetzt am Popover selbst: `MenuBarModule` bekommt
`popoverDidOpen`/`popoverDidClose`, gemeldet auch dann, wenn das Popover von
selbst zugeht — beim Wechsel in eine andere App etwa.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:20:07 +02:00
Scarriffle
c94c6166f5 Der Farbwechsel kommt jetzt auch an
Farbton und Symbolfarbe standen beim **Erzeugen** der Fenster, also außerhalb
jeder View. Damit wird der Wert genau einmal gelesen und SwiftUI erfährt nie,
dass er sich geändert hat: die Reiter, Knöpfe und Symbole blieben in der
Farbe stehen, die beim Öffnen galt, während der Farbwähler daneben schon die
neue zeigte.

Jetzt stehen beide im Rumpf der jeweiligen View — Panel, Einstellungen,
Einrichtung. Dort registriert SwiftUI den Zugriff und zeichnet neu.

Bei den Popovers blieb es beim Erzeugen: die entstehen bei jedem Öffnen neu,
lesen also ohnehin den aktuellen Wert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:18:06 +02:00
Scarriffle
1432ef3a6f Der Systemfarbknopf zeigt, welche Farbe er nimmt
Er tut, was draufsteht — nur weiß niemand auswendig, welche Farbe das System
gerade führt. Dann wirkt das Ergebnis wie ein Fehler statt wie eine Übernahme.
Jetzt sitzt der Farbfleck daneben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:00:56 +02:00
11 changed files with 119 additions and 19 deletions

View File

@@ -42,8 +42,16 @@ struct ColorSettingsView: View {
Spacer(minLength: 0)
}
// Mit dem Farbfleck daneben: welche Farbe das System gerade
// führt, weiß man nicht auswendig und dann wirkt das
// Ergebnis wie ein Fehler statt wie eine Übernahme.
Button { theme.adoptSystemAccent() } label: {
HStack(spacing: 6) {
Text("color.adoptSystem")
Circle()
.fill(Color(nsColor: .controlAccentColor))
.frame(width: 11, height: 11)
}
}
Text("color.accent.hint")
.font(.caption).foregroundStyle(.secondary)

View File

@@ -33,6 +33,15 @@ final class MixerMenuBarModule: MenuBarModule {
return view
}
/// Pegel nur, solange das Popover offen ist.
///
/// Der Mikrofonpegel schaltet den orangen Punkt an das darf keine
/// Sekunde länger dauern als das Hinsehen. An der View ließ es sich nicht
/// festmachen: `onDisappear` bleibt beim Popover aus, und der Punkt blieb
/// dauerhaft stehen.
func popoverDidOpen() { system.levels.addConsumer() }
func popoverDidClose() { system.levels.removeConsumer() }
func makePopoverView() -> AnyView {
AnyView(
ScrollView {

View File

@@ -82,6 +82,9 @@ struct OnboardingView: View {
footer
}
.frame(width: 520, height: 520)
// Im Rumpf gelesen, damit ein Farbwechsel ankommt.
.tint(Onyx.Color.accent)
.labelStyle(.onyx)
.task {
calendarGranted = calendarModel.hasAccess
fanControl.refreshInstallState()

View File

@@ -44,9 +44,7 @@ final class OnboardingWindowController: NSObject, NSWindowDelegate {
rootView: OnboardingView(
calendarModel: calendarModel, weatherModel: weatherModel,
mixer: mixer, fanControl: fanControl, launchAtLogin: launchAtLogin,
onFinish: { [weak self] in self?.finish() })
.tint(Onyx.Color.accent)
.labelStyle(.onyx))
onFinish: { [weak self] in self?.finish() }))
let window = NSWindow(contentViewController: hosting)
window.title = "Onyx"

View File

@@ -76,9 +76,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
NotchHostingView(
rootView: NotchContainer(presentation: presentation) {
NotchPanelContent(model: model)
}
.tint(Onyx.Color.accent)
.labelStyle(.onyx),
},
presentation: presentation)
}
coordinator.start()
@@ -366,5 +364,8 @@ private struct NotchPanelContent: View {
var body: some View {
WidgetGrid(placements: model.layout, availableWidth: AppModel.availableWidth)
// Im Rumpf gelesen, damit ein Farbwechsel ankommt.
.tint(Onyx.Color.accent)
.labelStyle(.onyx)
}
}

View File

@@ -71,6 +71,14 @@ struct SettingsView: View {
// Systems, und die beiden liegen in Dunkelgrau ein, zwei Prozent
// auseinander. Sichtbar wurde das als Kante quer durchs Fenster.
.background(Color(nsColor: .windowBackgroundColor))
// Farbton und Symbolfarbe hier und nicht beim Erzeugen des Fensters.
//
// Beim Erzeugen wird der Wert **einmal** gelesen, außerhalb jeder
// View SwiftUI erfährt nie, dass er sich geändert hat. Die Reiter
// und Knöpfe blieben deshalb in der Farbe stehen, die beim Öffnen
// galt, während der Farbwähler daneben schon die neue zeigte.
.tint(Onyx.Color.accent)
.labelStyle(.onyx)
// Nur ein Mindestmaß, keine feste Größe: sonst kann das Fenster
// wachsen, der Inhalt aber nicht mit.
.frame(minWidth: 740, minHeight: 460)

View File

@@ -61,12 +61,7 @@ final class SettingsWindowController: NSObject, NSWindowDelegate {
showOnboarding: showOnboarding,
selectedTab: Binding(
get: { [weak self] in self?.selectedTab ?? .widgets },
set: { [weak self] in self?.selectedTab = $0 }))
// Ohne das folgen Haken, Regler und Auswahlfelder der
// Systemfarbe statt der eingestellten. Genau daher kam das
// Grün, das in Onyx sonst nirgends vorkommt.
.tint(Onyx.Color.accent)
.labelStyle(.onyx))
set: { [weak self] in self?.selectedTab = $0 })))
let window = NSWindow(contentViewController: hosting)
window.title = "Onyx"

View File

@@ -196,6 +196,22 @@
}
}
},
"mixer.recordingHint": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Solange der Mixer läuft, zeigt macOS das orange Aufnahmesymbol: Ton abgreifen heißt für das System aufnehmen, auch wenn nur die Lautstärke geregelt wird."
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "While the mixer runs, macOS shows the orange recording dot: tapping audio counts as recording to the system, even when only the volume is adjusted."
}
}
}
},
"mixer.release": {
"localizations": {
"de": {

View File

@@ -86,11 +86,10 @@ public struct MixerView: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
.animation(Onyx.Motion.value, value: mixer.groups.count)
// Die Pegel laufen nur, solange die Ansicht sichtbar ist. Der
// Mikrofonpegel schaltet dabei den orangen Punkt an ein Messgerät,
// das im Hintergrund weiterläuft, wäre hier das Falsche.
.onAppear { system.levels.addConsumer() }
.onDisappear { system.levels.removeConsumer() }
// Kein `onAppear` für die Pegel: die Messung hängt am Popover, nicht
// an dieser View. `onDisappear` bleibt bei einem NSPopover aus, und
// ein Mikrofon, das nur beim Öffnen an- und nie wieder ausgeht, ist
// schlimmer als eines, das gar nicht misst.
}
}
@@ -107,6 +106,17 @@ extension MixerView {
}
.toggleStyle(.checkbox)
.controlSize(.small)
// Der Hinweis gehört hierher, weil hier die Entscheidung fällt.
//
// Ein Tap ist für macOS eine Tonaufnahme auch wenn nur die
// Lautstärke geregelt wird. Solange der Mixer läuft, steht deshalb das
// orange Aufnahmesymbol in der Leiste. Wer das ohne Erklärung sieht,
// sucht den Fehler bei sich oder hält die App für unsauber.
Text("mixer.recordingHint", bundle: .module)
.font(Onyx.Font.metricSmall)
.foregroundStyle(Onyx.Color.textTertiary)
.fixedSize(horizontal: false, vertical: true)
}
}

View File

@@ -10,6 +10,9 @@ import SwiftUI
@MainActor
public final class MenuBarController {
/// Hält den Delegaten am Leben `NSPopover` merkt ihn sich nur schwach.
private var popoverWatcher: PopoverWatcher?
/// Ein Modul, dessen Element keine Breite bekommen hat.
///
/// Die Menüleiste ist endlich, und auf einem Notch-Display ist die Mitte
@@ -148,7 +151,11 @@ public final class MenuBarController {
rootView: entry.module.makePopoverView()
.tint(Onyx.Color.accent)
.labelStyle(.onyx))
let watcher = PopoverWatcher { [weak self] in self?.popoverClosedByItself() }
popoverWatcher = watcher
entry.popover.delegate = watcher
entry.popover.show(relativeTo: sender.bounds, of: sender, preferredEdge: .minY)
entry.module.popoverDidOpen()
// Ohne das bleibt das Popover hinter dem aktiven Fenster.
entry.popover.contentViewController?.view.window?.makeKey()
openPopoverID = id
@@ -190,11 +197,21 @@ public final class MenuBarController {
}
}
/// Zugegangen, ohne dass wir es veranlasst haben.
private func popoverClosedByItself() {
guard let id = openPopoverID, let entry = entries[id] else { return }
entry.module.popoverDidClose()
openPopoverID = nil
}
private func closeOpenPopover() {
if let id = openPopoverID, let entry = entries[id] {
entry.popover.performClose(nil)
}
openPopoverID = nil
// Über denselben Weg wie das Zugehen von selbst: der räumt
// `openPopoverID` ab und ist damit genau einmal wirksam, egal ob der
// Delegat vorher, nachher oder gar nicht meldet.
popoverClosedByItself()
globalMonitor.map(NSEvent.removeMonitor)
localMonitor.map(NSEvent.removeMonitor)
keyMonitor.map(NSEvent.removeMonitor)
@@ -215,3 +232,21 @@ public final class MenuBarController {
onVisibilityChanged?(hidden)
}
}
/// Horcht, ob das Popover zugegangen ist.
///
/// Ein eigener Typ, weil ein Delegat von AppKit ein `NSObject` sein muss und
/// der Controller keines ist. Nötig ist er trotzdem: das Popover geht auch
/// ohne Zutun zu beim Wechsel in eine andere App etwa , und ohne diese
/// Meldung erfährt eine laufende Messung nie, dass niemand mehr hinsieht.
final class PopoverWatcher: NSObject, NSPopoverDelegate {
private let onClose: @MainActor () -> Void
init(onClose: @escaping @MainActor () -> Void) {
self.onClose = onClose
}
func popoverDidClose(_ notification: Notification) {
MainActor.assumeIsolated { onClose() }
}
}

View File

@@ -42,6 +42,17 @@ public protocol MenuBarModule: AnyObject {
/// Der Inhalt des Popovers beim Klick.
func makePopoverView() -> AnyView
/// Wird gerufen, wenn das Popover dieses Moduls auf- und wieder zugeht.
///
/// **Die einzige verlässliche Stelle für alles, was nur laufen darf,
/// solange jemand hinsieht.** SwiftUIs `onDisappear` bleibt bei einem
/// `NSPopover` aus: dessen Controller lebt nach dem Schließen weiter, die
/// View verschwindet nie aus der Hierarchie. Wer eine Messung daran hängt,
/// hängt sie an etwas, das nie eintritt im Fall des Mikrofons mit dem
/// orangen Punkt als Beweis.
func popoverDidOpen()
func popoverDidClose()
/// Wird gerufen, wenn das Modul sichtbar wird bzw. verschwindet hier
/// meldet sich das Modul beim Provider an und wieder ab.
func activate()
@@ -72,3 +83,9 @@ extension TimeInterval {
Swift.min(Swift.max(self, range.lowerBound), range.upperBound)
}
}
public extension MenuBarModule {
// Die meisten Module haben nichts abzuräumen.
func popoverDidOpen() {}
func popoverDidClose() {}
}