Files
Termind/TFM/UI/Components/WorkspaceDragCancellationTracker.swift

66 lines
2.0 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import AppKit
/// Observes cancellation without participating in the native List's gesture
/// recognition. Escape stays latched through its failed-drop return animation.
@MainActor
final class WorkspaceDragCancellationTracker {
private var observer: CFRunLoopObserver?
private var keyMonitor: Any?
private var wasCancelled = false
func begin() {
guard observer == nil else { return }
wasCancelled = false
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
if event.keyCode == 53 { self?.recordEscape() }
return event
}
// AppKit's drag tracking loop can consume Escape before local event
// monitors receive it. Observe key state within that loop as well.
observer = CFRunLoopObserverCreateWithHandler(
nil, CFRunLoopActivity.allActivities.rawValue, true, 0
) { [weak self] _, _ in
MainActor.assumeIsolated {
if CGEventSource.keyState(.combinedSessionState, key: 53) {
self?.recordEscape()
}
}
}
if let observer {
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
CFRunLoopAddObserver(
CFRunLoopGetMain(), observer,
CFRunLoopMode(rawValue: RunLoop.Mode.eventTracking.rawValue as CFString)
)
}
}
func recordEscape() {
wasCancelled = true
}
@discardableResult
func end() -> Bool {
stopObserving()
return wasCancelled
}
private func stopObserving() {
if let observer {
CFRunLoopObserverInvalidate(observer)
self.observer = nil
}
if let keyMonitor {
NSEvent.removeMonitor(keyMonitor)
self.keyMonitor = nil
}
}
isolated deinit {
stopObserving()
}
}