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. @MainActor final class SettingsWindowController: NSObject, NSWindowDelegate { 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)) 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: 780, height: 660)) // Breit genug für die Reiterleiste. // // Sechs Reiter mit deutschen Beschriftungen brauchen mehr, als man // beim Nachrechnen der reinen Textbreiten herausbekommt: macOS legt // je Reiter Innenabstand und Trenner dazu. Ist das Fenster zu // schmal, schneidet es die Leiste rechts einfach ab — der letzte // Reiter ist dann nicht mehr erreichbar. window.contentMinSize = NSSize(width: 740, 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) } }