Files
onyx/Onyx/SettingsWindowController.swift
Scarriffle 784b210bc2 Vorschau mit Schreibtischhintergrund, Fenster breit genug für die Reiter
Die Vorschau der Auslösefläche war eine schwarze Fläche — als Bildschirm
kaum zu erkennen. Jetzt liegt der echte Schreibtischhintergrund darunter.
Apples eigene Bilder lassen sich nicht mitliefern (Lizenz, und ein
Tahoe-Hintergrund wiegt zweistellige Megabyte); der eigene ist ohnehin der
bessere Anker, weil man genau ihn wiedererkennt. Fehlt er, bleibt ein
Verlauf in Onyx-Farben.

Dabei fiel auf, dass Notch und Auslösefläche oberhalb des Bildschirms
schwebten statt in ihm zu sitzen: die Rundung lag auf dem Hintergrund
allein, nicht auf dem Ganzen. Sie schneidet jetzt Hintergrund, Menüleiste
und Fläche gemeinsam ab.

Und das Einstellungsfenster war zu schmal für sechs Reiter — die Leiste
wurde rechts abgeschnitten, „Berechtigungen" war nicht mehr erreichbar. 780
statt 620 Punkte, Mindestbreite 740. Beim Nachrechnen der reinen Textbreiten
kommt man auf 560 und liegt damit deutlich daneben: macOS legt je Reiter
Innenabstand und Trenner dazu.

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

132 lines
6.2 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"
// 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)
}
}