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>
95 lines
3.6 KiB
Swift
95 lines
3.6 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
import CalendarProvider
|
|
import WeatherProvider
|
|
import AudioProvider
|
|
|
|
/// Zeigt das Onboarding — einmal, beim ersten Start.
|
|
///
|
|
/// Als eigenes Fenster und nicht im Panel: TCC blendet Berechtigungsdialoge
|
|
/// stillschweigend aus, wenn keine App im Vordergrund steht, und eine
|
|
/// `.accessory`-App mit ausgefahrenem Panel steht nicht im Vordergrund. Genau
|
|
/// daran ist die Kalenderberechtigung schon einmal gescheitert.
|
|
@MainActor
|
|
final class OnboardingWindowController: NSObject, NSWindowDelegate {
|
|
|
|
private static let doneKey = "onboarding.completed"
|
|
|
|
private var window: NSWindow?
|
|
private let defaults: UserDefaults
|
|
|
|
init(defaults: UserDefaults = .standard) {
|
|
self.defaults = defaults
|
|
}
|
|
|
|
var isDone: Bool { defaults.bool(forKey: Self.doneKey) }
|
|
|
|
func showIfNeeded(calendarModel: CalendarModel, weatherModel: WeatherModel,
|
|
mixer: AudioMixer, fanControl: FanControl,
|
|
launchAtLogin: LaunchAtLogin) {
|
|
guard !isDone else { return }
|
|
show(calendarModel: calendarModel, weatherModel: weatherModel, mixer: mixer,
|
|
fanControl: fanControl, launchAtLogin: launchAtLogin)
|
|
}
|
|
|
|
func show(calendarModel: CalendarModel, weatherModel: WeatherModel,
|
|
mixer: AudioMixer, fanControl: FanControl, launchAtLogin: LaunchAtLogin) {
|
|
// Solange dieses Fenster steht, gehört Onyx in den Dock und in den
|
|
// Vordergrund — sonst erscheinen die Systemdialoge gar nicht.
|
|
NSApp.setActivationPolicy(.regular)
|
|
|
|
if window == nil {
|
|
let hosting = NSHostingController(
|
|
rootView: OnboardingView(
|
|
calendarModel: calendarModel, weatherModel: weatherModel,
|
|
mixer: mixer, fanControl: fanControl, launchAtLogin: launchAtLogin,
|
|
onFinish: { [weak self] in self?.finish() }))
|
|
|
|
let window = NSWindow(contentViewController: hosting)
|
|
window.title = "Onyx"
|
|
window.styleMask = [.titled, .closable]
|
|
window.isReleasedWhenClosed = false
|
|
window.delegate = self
|
|
// Nicht direkt unter die Notch: wer den Schließknopf trifft,
|
|
// fährt sonst durch die Auslösefläche.
|
|
if let screen = NSScreen.main {
|
|
let visible = screen.visibleFrame
|
|
window.setFrameOrigin(NSPoint(x: visible.midX - window.frame.width / 2,
|
|
y: visible.maxY - 60 - window.frame.height))
|
|
} else {
|
|
window.center()
|
|
}
|
|
self.window = window
|
|
}
|
|
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
window?.makeKeyAndOrderFront(nil)
|
|
}
|
|
|
|
/// Auch das Schließen über den roten Knopf gilt als erledigt. Ein Fenster,
|
|
/// das beim nächsten Start wiederkommt, weil man es weggeklickt hat, ist
|
|
/// eine Zumutung.
|
|
private func finish() {
|
|
defaults.set(true, forKey: Self.doneKey)
|
|
hide()
|
|
}
|
|
|
|
/// Wie bei den Einstellungen: ausblenden statt schließen, damit ein
|
|
/// Hilfsprogramm mit „letztes Fenster zu = App beenden" Onyx nicht mit
|
|
/// wegräumt.
|
|
func windowShouldClose(_ sender: NSWindow) -> Bool {
|
|
defaults.set(true, forKey: Self.doneKey)
|
|
hide()
|
|
return false
|
|
}
|
|
|
|
private func hide() {
|
|
window?.orderOut(nil)
|
|
// Zurück in den Hintergrund — es sei denn, ein anderes Fenster steht
|
|
// noch offen.
|
|
if !NSApp.windows.contains(where: { $0 !== window && $0.isVisible }) {
|
|
NSApp.setActivationPolicy(.accessory)
|
|
}
|
|
}
|
|
}
|