Absturz beim XPC-Rückruf, Menü zurück, Fenster weg von der Notch
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>
This commit is contained in:
@@ -86,7 +86,7 @@ public final class CalendarModel {
|
||||
let wanted = kind != .appleCalendar
|
||||
guard wanted != (calendarrObserver != nil) else { return }
|
||||
calendarrObserver = wanted
|
||||
? DarwinObserver(name: CalendarrSource.changeNotification) { [weak self] in
|
||||
? DarwinObserver(name: CalendarrSource.changeNotification) { @Sendable [weak self] in
|
||||
Task { @MainActor in self?.refresh() }
|
||||
}
|
||||
: nil
|
||||
|
||||
@@ -64,7 +64,7 @@ public final class AdapterProcess {
|
||||
Task { @MainActor in self?.consume(chunk) }
|
||||
}
|
||||
|
||||
process.terminationHandler = { [weak self] _ in
|
||||
process.terminationHandler = { @Sendable [weak self] _ in
|
||||
Task { @MainActor in self?.handleTermination() }
|
||||
}
|
||||
|
||||
|
||||
@@ -130,18 +130,19 @@ public final class NotchCoordinator {
|
||||
return event
|
||||
}) { monitors.append(local) }
|
||||
|
||||
// Klicks in der eigenen App: in der Notch fixieren, sonst schließen.
|
||||
if let clicks = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown], handler: { event in
|
||||
MainActor.assumeIsolated { [weak self] in
|
||||
guard let self else { return }
|
||||
let point = NSEvent.mouseLocation
|
||||
for controller in controllers.values
|
||||
where controller.isVisible && controller.geometry.triggerRect.contains(point) {
|
||||
controller.click()
|
||||
}
|
||||
}
|
||||
MainActor.assumeIsolated { [weak self] in self?.handleClick() }
|
||||
return event
|
||||
}) { monitors.append(clicks) }
|
||||
|
||||
// Klicks in fremden Programmen schließen ein fixiertes Panel. Ohne das
|
||||
// bleibt es stehen, bis jemand Escape drückt oder die Regel kennt.
|
||||
if let outside = NSEvent.addGlobalMonitorForEvents(
|
||||
matching: [.leftMouseDown, .rightMouseDown], handler: { _ in
|
||||
MainActor.assumeIsolated { [weak self] in self?.handleClick() }
|
||||
}) { monitors.append(outside) }
|
||||
|
||||
if let keys = NSEvent.addLocalMonitorForEvents(matching: [.keyDown], handler: { event in
|
||||
guard event.keyCode == 53 else { return event } // esc
|
||||
MainActor.assumeIsolated { [weak self] in
|
||||
@@ -151,6 +152,20 @@ public final class NotchCoordinator {
|
||||
}) { monitors.append(keys) }
|
||||
}
|
||||
|
||||
/// Ein Klick fixiert nur in der Notch selbst. Überall sonst schließt er —
|
||||
/// auch das eigene Panel, denn wer hineinklickt, bedient ein Widget und
|
||||
/// nicht das Panel.
|
||||
private func handleClick() {
|
||||
let point = NSEvent.mouseLocation
|
||||
for controller in controllers.values where controller.isVisible {
|
||||
if controller.geometry.triggerRect.contains(point) {
|
||||
controller.click()
|
||||
} else if !controller.panelFrame.contains(point) {
|
||||
controller.clickedOutside()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func pointerMoved() {
|
||||
let point = NSEvent.mouseLocation
|
||||
for controller in controllers.values {
|
||||
|
||||
@@ -81,6 +81,9 @@ public final class NotchScreenController {
|
||||
}
|
||||
|
||||
public func click() { send(.click) }
|
||||
public func clickedOutside() { send(.clickedOutside) }
|
||||
/// Für den Koordinator: wo das Panel gerade steht.
|
||||
public var panelFrame: CGRect { panel.frame }
|
||||
public func escape() { send(.escape) }
|
||||
public func setSuppressed(_ suppressed: Bool) {
|
||||
guard suppressed != machine.isSuppressed else { return }
|
||||
|
||||
@@ -20,6 +20,12 @@ public enum NotchInput: Equatable, Sendable {
|
||||
case pointerEntered
|
||||
case pointerExited
|
||||
case click
|
||||
/// Klick irgendwo außerhalb von Panel und Notch.
|
||||
///
|
||||
/// Der Ausweg aus einem fixierten Panel. „Klick fixiert" ist eine gute
|
||||
/// Regel, aber wer sie nicht kennt, sitzt sonst vor einem Panel, das offen
|
||||
/// steht und auf nichts reagiert.
|
||||
case clickedOutside
|
||||
case escape
|
||||
case armTimerFired
|
||||
case closeTimerFired
|
||||
@@ -173,9 +179,15 @@ public struct NotchStateMachine: Equatable, Sendable {
|
||||
return []
|
||||
|
||||
// Beenden
|
||||
case (.open, .escape), (.pinned, .escape), (.closing, .escape):
|
||||
case (.open, .escape), (.pinned, .escape), (.closing, .escape),
|
||||
(.open, .clickedOutside), (.pinned, .clickedOutside), (.closing, .clickedOutside):
|
||||
phase = .idle
|
||||
return [.hide]
|
||||
return [.cancelCloseTimer, .hide]
|
||||
|
||||
case (.arming, .clickedOutside):
|
||||
phase = .idle
|
||||
debounceElapsed = false
|
||||
return [.cancelArmTimer, .cancelCloseTimer]
|
||||
|
||||
case (.arming, .escape):
|
||||
phase = .idle
|
||||
|
||||
@@ -106,7 +106,7 @@ public final class ShelfStore {
|
||||
public func remove(_ item: ShelfItem) {
|
||||
// In den Papierkorb statt endgültig: die Ablage ist ein Zwischenlager,
|
||||
// und ein Fehlklick darf keine Datei vernichten.
|
||||
NSWorkspace.shared.recycle([item.url]) { [weak self] _, error in
|
||||
NSWorkspace.shared.recycle([item.url]) { @Sendable [weak self] _, error in
|
||||
if let error { log.error("Ablage: nicht gelöscht: \(error.localizedDescription, privacy: .public)") }
|
||||
Task { @MainActor in self?.reload() }
|
||||
}
|
||||
@@ -114,7 +114,7 @@ public final class ShelfStore {
|
||||
|
||||
public func removeAll() {
|
||||
guard !items.isEmpty else { return }
|
||||
NSWorkspace.shared.recycle(items.map(\.url)) { [weak self] _, _ in
|
||||
NSWorkspace.shared.recycle(items.map(\.url)) { @Sendable [weak self] _, _ in
|
||||
Task { @MainActor in self?.reload() }
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ public final class ShelfStore {
|
||||
let expired = ShelfCleanup.expired(items, now: now, afterDays: autoCleanupDays)
|
||||
guard !expired.isEmpty else { return }
|
||||
log.notice("Ablage: \(expired.count, privacy: .public) Dateien abgelaufen")
|
||||
NSWorkspace.shared.recycle(expired.map(\.url)) { [weak self] _, _ in
|
||||
NSWorkspace.shared.recycle(expired.map(\.url)) { @Sendable [weak self] _, _ in
|
||||
Task { @MainActor in self?.reload() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ final class ThumbnailCache {
|
||||
fileAt: item.url, size: CGSize(width: size, height: size),
|
||||
scale: NSScreen.main?.backingScaleFactor ?? 2,
|
||||
representationTypes: .all)
|
||||
QLThumbnailGenerator.shared.generateBestRepresentation(for: request) { rep, _ in
|
||||
QLThumbnailGenerator.shared.generateBestRepresentation(for: request) { @Sendable rep, _ in
|
||||
guard let rep else { return }
|
||||
let image = NSImage(cgImage: rep.cgImage,
|
||||
size: CGSize(width: size, height: size))
|
||||
|
||||
Reference in New Issue
Block a user