Files
onyx/Onyx/SettingsWindowController.swift
Scarriffle ab96086db7 Reiterleiste liegt nicht mehr auf der Trennlinie
Sie saß genau auf der Kante unter dem Titelbalken. `TabView` in einem
gewöhnlichen Fenster bekommt den Abstand nicht von selbst — in Apples
`Settings`-Szene schon, und die benutzt Onyx bewusst nicht, weil sie über
einen privaten Selektor öffnet und bei Apps ohne Menüleiste unzuverlässig
ankommt.

Zwölf Punkte Abstand nach oben, und der Titelbalken geht durchsichtig in den
Inhalt über, statt ihn mit einer Linie abzuschneiden.

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

136 lines
6.4 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, statt ihn mit einer
// Linie abzuschneiden sonst liegt die Reiterleiste direkt auf
// dieser Linie.
window.titlebarAppearsTransparent = true
// 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)
}
}