Zwei Folgen der eigenen Reiterleiste, beide meine Schuld. Der Reiter oben sprang um, der Inhalt blieb stehen: der gewählte Reiter liegt im Fenster-Controller — er muss dort liegen, damit ein erneutes Öffnen ihn setzen kann, etwa auf „Berechtigungen", wenn eine fehlt. Der Controller war aber nicht beobachtbar. `TabView` fiel das nie auf, weil es seinen Zustand selbst führte und die Bindung nur benachrichtigte. Ein `switch` im Rumpf braucht dagegen eine Änderungsmeldung, und die kam nie. Jetzt ist der Controller `@Observable`, und beide Richtungen funktionieren. Zu breit war sie wegen `fixedSize`: sieben deutsche Beschriftungen brauchen rund 880 Punkte, und die nahm sich die Leiste auch dann, wenn das Fenster 780 breit war — sie ragte links und rechts über die Kante hinaus, der erste und der letzte Reiter waren nicht mehr erreichbar. Ohne feste Breite bleibt sie im Fenster, und das Fenster ist jetzt 920 breit statt 780, mit 900 als Mindestmaß. Darunter würde gestaucht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
154 lines
7.4 KiB
Swift
154 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))
|
|
|
|
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)
|
|
}
|
|
}
|