Compare commits
3 Commits
9975694d6a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee848b86cc | ||
|
|
c94c6166f5 | ||
|
|
1432ef3a6f |
@@ -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: {
|
||||
Text("color.adoptSystem")
|
||||
HStack(spacing: 6) {
|
||||
Text("color.adoptSystem")
|
||||
Circle()
|
||||
.fill(Color(nsColor: .controlAccentColor))
|
||||
.frame(width: 11, height: 11)
|
||||
}
|
||||
}
|
||||
Text("color.accent.hint")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user