Die Leiste saß mittig auf der Trennlinie unter dem Titelbalken. `titlebarAppearsTransparent` allein reichte nicht — macOS zieht darunter zusätzlich einen Trenner quer durchs Fenster. Der ist jetzt aus. Man setzt keinen Titel auf eine Trennlinie. „Quellen" war die Sicht des Programmierers: für den Nutzer ist die Datenquelle eine Einstellung **des Kalenderwidgets** unter mehreren. Der Bereich heißt jetzt „Widget-Optionen" und hat einen Abschnitt je Widget. Kalender: woher die Daten kommen, Monatsraster oder Terminliste — und endlich, welche Kalender überhaupt mitzählen. Gespeichert werden die **abgewählten**, nicht die gewählten: kommt ein neuer Kalender dazu, ist er damit automatisch dabei, statt dass man ihn suchen müsste, ohne zu wissen, dass es ihn gibt. Der Filter lässt die Abdeckung unberührt — welche Kalender man sehen will, sagt nichts darüber aus, für welchen Zeitraum Daten vorliegen. Wetter: dem Standort folgen oder einen festen Ort setzen, per Stadt oder Postleitzahl. WeatherKit will Koordinaten, der Nutzer kennt Ortsnamen — die Umsetzung passiert einmal beim Einstellen statt bei jedem Abruf. Der Ort überlebt jetzt auch den Neustart; vorher stand nach jedem Start wieder der Aufenthaltsort da. Die Ablage-Einstellungen sind vom Widget-Reiter hierher gewandert, wo die anderen Widget-Optionen stehen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
141 lines
6.7 KiB
Swift
141 lines
6.7 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
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 })))
|
|
|
|
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)
|
|
}
|
|
}
|