Files
onyx/Onyx/OnboardingWindowController.swift
Scarriffle 0df566d531 Symbole folgen der Akzentfarbe, nicht der des Systems
SwiftUI zeichnet Symbole in Listen und Formularen auf macOS in der
Akzentfarbe des Systems — nicht in der des Fensters. `.tint` an der Wurzel
half deshalb nur der Reiterleiste; die Widget-Liste blieb systemgrün neben
einer blauen Leiste, obwohl beides derselben Einstellung folgen soll.

Ein eigener Label-Stil setzt die Farbe am Symbol. Er vererbt sich über die
Umgebung, also genügt eine Zeile an denselben vier Wurzeln, an denen auch der
Farbton gesetzt wird: Panel, Popovers, Einstellungen, Einrichtung.

Ein Symbol, das seine Farbe selbst mitbringt, behält sie — die innere Angabe
schlägt die äußere. Der grüne Haken für „erteilt" bringt sie jetzt am Symbol
mit statt am ganzen Label: Grün heißt dort „erteilt" und ist keine
Geschmacksfrage.

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

98 lines
3.7 KiB
Swift

import AppKit
import SwiftUI
import OnyxDesign
import CalendarProvider
import WeatherProvider
import AudioProvider
/// Zeigt das Onboarding einmal, beim ersten Start.
///
/// Als eigenes Fenster und nicht im Panel: TCC blendet Berechtigungsdialoge
/// stillschweigend aus, wenn keine App im Vordergrund steht, und eine
/// `.accessory`-App mit ausgefahrenem Panel steht nicht im Vordergrund. Genau
/// daran ist die Kalenderberechtigung schon einmal gescheitert.
@MainActor
final class OnboardingWindowController: NSObject, NSWindowDelegate {
private static let doneKey = "onboarding.completed"
private var window: NSWindow?
private let defaults: UserDefaults
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
}
var isDone: Bool { defaults.bool(forKey: Self.doneKey) }
func showIfNeeded(calendarModel: CalendarModel, weatherModel: WeatherModel,
mixer: AudioMixer, fanControl: FanControl,
launchAtLogin: LaunchAtLogin) {
guard !isDone else { return }
show(calendarModel: calendarModel, weatherModel: weatherModel, mixer: mixer,
fanControl: fanControl, launchAtLogin: launchAtLogin)
}
func show(calendarModel: CalendarModel, weatherModel: WeatherModel,
mixer: AudioMixer, fanControl: FanControl, launchAtLogin: LaunchAtLogin) {
// Solange dieses Fenster steht, gehört Onyx in den Dock und in den
// Vordergrund sonst erscheinen die Systemdialoge gar nicht.
NSApp.setActivationPolicy(.regular)
if window == nil {
let hosting = NSHostingController(
rootView: OnboardingView(
calendarModel: calendarModel, weatherModel: weatherModel,
mixer: mixer, fanControl: fanControl, launchAtLogin: launchAtLogin,
onFinish: { [weak self] in self?.finish() })
.tint(Onyx.Color.accent)
.labelStyle(.onyx))
let window = NSWindow(contentViewController: hosting)
window.title = "Onyx"
window.styleMask = [.titled, .closable]
window.isReleasedWhenClosed = false
window.delegate = self
// Nicht direkt unter die Notch: wer den Schließknopf trifft,
// fährt sonst durch die Auslösefläche.
if let screen = NSScreen.main {
let visible = screen.visibleFrame
window.setFrameOrigin(NSPoint(x: visible.midX - window.frame.width / 2,
y: visible.maxY - 60 - window.frame.height))
} else {
window.center()
}
self.window = window
}
NSApp.activate(ignoringOtherApps: true)
window?.makeKeyAndOrderFront(nil)
}
/// Auch das Schließen über den roten Knopf gilt als erledigt. Ein Fenster,
/// das beim nächsten Start wiederkommt, weil man es weggeklickt hat, ist
/// eine Zumutung.
private func finish() {
defaults.set(true, forKey: Self.doneKey)
hide()
}
/// Wie bei den Einstellungen: ausblenden statt schließen, damit ein
/// Hilfsprogramm mit letztes Fenster zu = App beenden" Onyx nicht mit
/// wegräumt.
func windowShouldClose(_ sender: NSWindow) -> Bool {
defaults.set(true, forKey: Self.doneKey)
hide()
return false
}
private func hide() {
window?.orderOut(nil)
// Zurück in den Hintergrund es sei denn, ein anderes Fenster steht
// noch offen.
if !NSApp.windows.contains(where: { $0 !== window && $0.isVisible }) {
NSApp.setActivationPolicy(.accessory)
}
}
}