Files
onyx/Onyx/SettingsWindowController.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

155 lines
7.4 KiB
Swift

import AppKit
import SwiftUI
import OnyxDesign
import CalendarProvider
import WeatherProvider
import MetricsProvider
import ShelfProvider
/// Führt das Einstellungsfenster selbst.
///
/// SwiftUIs `Settings`-Szene wird über `NSApp.sendAction(showSettingsWindow:)`
/// geöffnet ein privater Selektor, dessen Name sich zwischen macOS-Versionen
/// schon geändert hat (`showPreferencesWindow:` davor) und der bei
/// `.accessory`-Apps ohne Menüleiste unzuverlässig ankommt. Genau das ist hier
/// passiert: der Menüeintrag tat nichts.
///
/// Ein eigenes `NSWindow` hat diese Abhängigkeit nicht. Es kostet ein paar
/// Zeilen mehr und funktioniert dafür.
///
/// `@Observable`, und das ist keine Kür: der gewählte Reiter liegt hier, weil
/// ein erneutes Öffnen ihn setzen können muss. Ohne Beobachtung bemerkt die
/// View die Änderung nicht weder die von außen noch die eigene. Genau daran
/// hing das Fenster, in dem der Reiter oben umsprang und der Inhalt stehen
/// blieb.
@MainActor
@Observable
final class SettingsWindowController: NSObject, NSWindowDelegate {
@ObservationIgnored private var window: NSWindow?
/// Muss außerhalb der View liegen, damit ein erneutes Öffnen den Reiter setzen kann.
private var selectedTab: SettingsTab = .widgets
/// Ob Onyx dauerhaft im Dock steht. Bestimmt, wohin nach dem Schließen
/// zurückgeschaltet wird.
var keepsDockIcon = false
func show(model: AppModel, calendarModel: CalendarModel, weatherModel: WeatherModel,
metricsModel: MetricsModel?, shelfStore: ShelfStore?, fanControl: FanControl,
launchAtLogin: LaunchAtLogin, showOnboarding: @escaping () -> Void,
tab: SettingsTab = .widgets) {
selectedTab = tab
keepsDockIcon = model.showsDockIcon
// Solange ein Fenster offen ist, gehört Onyx in den Dock.
//
// Als reine `.accessory`-App hat sie dort keinen Eintrag das ist im
// Hintergrund richtig, aber mit sichtbarem Fenster verwirrend: das
// Fenster lässt sich dann weder über den Dock noch über -Tab
// zurückholen, wenn es hinter etwas anderes rutscht.
NSApp.setActivationPolicy(.regular)
if window == nil {
let hosting = NSHostingController(
rootView: SettingsView(model: model,
calendarModel: calendarModel,
weatherModel: weatherModel,
metricsModel: metricsModel,
shelfStore: shelfStore,
fanControl: fanControl,
launchAtLogin: launchAtLogin,
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))
let window = NSWindow(contentViewController: hosting)
window.title = "Onyx"
// Der Titelbalken geht in den Inhalt über, **und** seine Trennlinie
// verschwindet.
//
// `titlebarAppearsTransparent` allein reichte nicht: macOS zieht
// unter dem Titelbalken zusätzlich einen Trenner quer durchs
// Fenster, und die Reiterleiste saß mittig darauf. Man setzt keinen
// Titel auf eine Trennlinie.
window.titlebarAppearsTransparent = true
window.titlebarSeparatorStyle = .none
// Veränderbar, und das ist keine Kür: der Bereich Displays" mit
// der Vorschau der Auslösefläche passt in 420 Punkte Höhe nicht
// mehr, und ein Fenster, in dem man scrollen muss, obwohl der
// Bildschirm frei ist, ist eine Zumutung.
window.styleMask = [.titled, .closable, .miniaturizable, .resizable]
window.setContentSize(NSSize(width: 920, height: 680))
// Breit genug für die Reiterleiste.
//
// Sieben Reiter mit deutschen Beschriftungen brauchen rund 880
// Punkte mehr, als man beim Nachrechnen der reinen Textbreiten
// herausbekommt: macOS legt je Reiter Innenabstand und Trenner
// dazu. Ist das Fenster schmaler, ragt die Leiste über beide
// Kanten hinaus, und der erste wie der letzte Reiter sind nicht
// mehr erreichbar.
window.contentMinSize = NSSize(width: 900, height: 460)
window.isReleasedWhenClosed = false
window.delegate = self
Self.place(window)
// Über die Position freuen sich Nutzer mit mehreren Displays:
// beim zweiten Öffnen steht es wieder da, wo sie es hingeschoben haben.
window.setFrameAutosaveName("onyx.settings")
self.window = window
}
// Ohne `activate` bleibt das Fenster einer .accessory-App hinter der
// aktiven App liegen und wirkt, als wäre nichts passiert.
NSApp.activate(ignoringOtherApps: true)
window?.makeKeyAndOrderFront(nil)
}
/// Setzt das Fenster so, dass seine Titelleiste **nicht** an der Notch
/// klebt.
///
/// `center()` setzt etwas oberhalb der Mitte bei einem 452 Punkt hohen
/// Fenster landet der Schließknopf damit knapp unter der Menüleiste. Wer
/// ihn treffen will, fährt durch die Auslösefläche der Notch und hat
/// plötzlich das Panel vor sich.
private static func place(_ window: NSWindow) {
guard let screen = window.screen ?? NSScreen.main else { window.center(); return }
let visible = screen.visibleFrame
let size = window.frame.size
// Wirklich mittig, nicht etwas unterhalb der Notch".
//
// `center()` setzt oberhalb der Bildschirmmitte, und mein erster
// Versuch mit 60 Punkten Abstand war zu zaghaft: der Titelbalken lag
// weiter so hoch, dass man auf dem Weg zu seinen Knöpfen durch die
// Auslösefläche der Notch fuhr und aus Versehen das Panel öffnete.
window.setFrameOrigin(NSPoint(x: visible.midX - size.width / 2,
y: visible.midY - size.height / 2))
}
/// Der rote Knopf blendet aus, statt zu schließen.
///
/// Onyx ist eine App ohne Hauptfenster; Fenster zu" heißt hier nie
/// fertig". Manche Hilfsprogramme erzwingen aber genau diese Gleichung und
/// beenden eine App, sobald ihr letztes Fenster verschwindet dann wäre
/// Onyx nach einem Blick in die Einstellungen weg, samt Menüleiste und
/// Notch. Ein Fenster, das nie wirklich schließt, gibt dafür keinen Anlass.
func windowShouldClose(_ sender: NSWindow) -> Bool {
hide()
return false
}
private func hide() {
window?.orderOut(nil)
// Zurück in den Hintergrund es sei denn, der Dock-Eintrag ist
// ausdrücklich gewünscht.
if !keepsDockIcon {
NSApp.setActivationPolicy(.accessory)
}
NSApp.hide(nil)
}
}