Der Blitz fadet beim Laden zwischen Weiß und Schwarz — so gewünscht, und ich hätte vorher fragen sollen statt eine eigene Lösung zu bauen und sie hinterher zu begründen. Über den Kosinus statt linear: ein linearer Ping-Pong knickt an den Umkehrpunkten sichtbar. Der Zeitgeber läuft mit 30 Bildern je Sekunde und nur, solange geladen wird — steht das Kabel nicht drin, läuft gar nichts. Der Stecker für „am Netz, lädt nicht" bleibt ausgestanzt; dort bewegt sich ohnehin nichts. Das Einstellungsfenster lässt sich jetzt vergrößern und startet mit 620 × 620 statt 520 × 420. Der Bereich „Displays" passt mit der Vorschau der Auslösefläche nicht mehr in die alte Höhe, und ein Fenster, in dem man scrollt, obwohl der Bildschirm frei ist, ist eine Zumutung. Die feste Größe in der View musste dafür weichen — sonst wächst das Fenster und der Inhalt bleibt stehen. Und es öffnet jetzt wirklich mittig. Mein erster Versuch mit 60 Punkten Abstand von oben war zu zaghaft: der Titelbalken lag weiter so hoch, dass man auf dem Weg zu seinen Knöpfen durch die Auslösefläche fuhr. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
5.8 KiB
Swift
125 lines
5.8 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: 620, height: 620))
|
|
window.contentMinSize = NSSize(width: 520, height: 420)
|
|
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)
|
|
}
|
|
}
|