Der Absturzbericht war eindeutig:
Thread 8: com.apple.NSXPCConnection.m-user…onyx.helper
swift_task_isCurrentExecutorWithFlags
closure #3 in FanControl.proxy()
EXC_BREAKPOINT
Die Rückrufe der XPC-Verbindung erben die MainActor-Isolation der Methode,
in der sie stehen. XPC ruft sie aber auf seiner eigenen Warteschlange auf,
Swift 6 prüft das zur Laufzeit und beendet den Prozess. Das
`Task { @MainActor in … }` im Rumpf half nicht: die Prüfung geschieht beim
Betreten des Abschlusses, nicht beim Zugriff.
Alle sieben Rückrufe sind jetzt `@Sendable`. Und weil das eine Fehlerklasse
ist und kein Einzelfall, dieselbe Behandlung für die übrigen Stellen, an
denen ein MainActor-Typ einen Abschluss an eine Systemschnittstelle gibt:
Papierkorb, Vorschaubilder, Adapter-Ende, Darwin-Nachricht.
Dazu drei Dinge aus dem Bericht von eben:
Der Linksklick aufs Menüleistensymbol fuhr das Panel aus — und ging dabei
als Fixieren durch. Danach stand das Panel offen und reagierte auf nichts
mehr. Das war schlechter als das Problem, das es lösen sollte. Ein
Statuselement zeigt bei einem Klick sein Menü; alles andere überrascht.
„Panel öffnen" bleibt draußen.
Ein Klick daneben schließt jetzt auch ein fixiertes Panel. „Klick fixiert"
ist eine gute Regel, aber wer sie nicht kennt, sitzt sonst vor etwas, das
offen steht und nicht reagiert — und sucht den Fehler in der App.
Einstellungs- und Einrichtungsfenster gehen nicht mehr direkt unter der
Notch auf. `center()` setzt oberhalb der Mitte; der Schließknopf landete
damit so weit oben, dass man auf dem Weg dorthin die Notch auslöste.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
5.1 KiB
Swift
116 lines
5.1 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"
|
|
window.styleMask = [.titled, .closable, .miniaturizable]
|
|
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
|
|
// Ein Fingerbreit Abstand nach oben genügt: die Auslösefläche reicht
|
|
// 14 Punkte unter die Notch, der Titelbalken ist 28 hoch.
|
|
let top = visible.maxY - 60
|
|
window.setFrameOrigin(NSPoint(x: visible.midX - size.width / 2,
|
|
y: top - size.height))
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
}
|