Files
Termind/TFM/Platform/AppReviewWindowReader.swift
T

145 lines
5.2 KiB
Swift

import AppKit
import SwiftUI
import TermindApplication
/// Native focus and input belong to the window, including controls hosted outside
/// SwiftUI. A local event monitor observes input without consuming or storing it.
struct AppReviewWindowReader: NSViewRepresentable {
let controller: AppReviewController
let location: AppReviewController.Location
let isBlocked: Bool
let request: () -> Void
func makeNSView(context: Context) -> ReviewActivityView {
ReviewActivityView(controller: controller)
}
func updateNSView(_ view: ReviewActivityView, context: Context) {
view.location = location
view.isBlocked = isBlocked
view.request = request
view.refresh()
}
static func dismantleNSView(_ view: ReviewActivityView, coordinator: ()) {
view.stop()
}
}
final class ReviewActivityView: NSView {
private let controller: AppReviewController
private let windowID = UUID()
var location: AppReviewController.Location = .other
var isBlocked = false
var request: () -> Void = {}
private var observers: [any NSObjectProtocol] = []
private var sleepObservers: [any NSObjectProtocol] = []
private var monitor: Any?
private var timer: Timer?
private var isSleeping = false
init(controller: AppReviewController) {
self.controller = controller
super.init(frame: .zero)
}
@available(*, unavailable)
required init?(coder: NSCoder) { nil }
isolated deinit { stop() }
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
stop()
guard window != nil else { return }
observers.append(
NotificationCenter.default.addObserver(
forName: NSWindow.willCloseNotification, object: window, queue: .main
) { [weak self] _ in MainActor.assumeIsolated { self?.stop() } }
)
for name in [
NSWindow.didBecomeKeyNotification, NSWindow.didResignKeyNotification,
NSWindow.didMiniaturizeNotification, NSWindow.didDeminiaturizeNotification,
NSWindow.willBeginSheetNotification, NSWindow.didEndSheetNotification,
NSApplication.didBecomeActiveNotification, NSApplication.didResignActiveNotification,
] {
observers.append(
NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) {
[weak self] _ in
MainActor.assumeIsolated {
if let self, name == NSWindow.willBeginSheetNotification {
self.controller.recordInteraction(surfaceID: self.windowID)
}
self?.refresh()
}
}
)
}
for name in [NSWorkspace.willSleepNotification, NSWorkspace.didWakeNotification] {
sleepObservers.append(
NSWorkspace.shared.notificationCenter.addObserver(forName: name, object: nil, queue: .main) {
[weak self] _ in
MainActor.assumeIsolated {
self?.isSleeping = name == NSWorkspace.willSleepNotification
self?.refresh()
}
}
)
}
monitor = NSEvent.addLocalMonitorForEvents(
matching: [
.keyDown, .flagsChanged, .leftMouseDown, .rightMouseDown, .otherMouseDown,
.leftMouseDragged, .rightMouseDragged, .otherMouseDragged, .scrollWheel, .mouseMoved,
]
) { [weak self] event in
MainActor.assumeIsolated {
if let self, self.window?.isKeyWindow == true {
self.controller.recordInteraction(surfaceID: self.windowID)
}
}
return event
}
refresh()
}
func refresh() {
let active =
window.map {
NSApp.isActive && $0.isKeyWindow && $0.isVisible && !$0.isMiniaturized && !isSleeping
} ?? false
controller.update(
surfaceID: windowID,
context: .init(
location: location, isActive: active,
isBlocked: isBlocked || window?.attachedSheet != nil || NSApp.modalWindow != nil
)
)
if active {
if timer == nil {
let timer = Timer(timeInterval: 0.5, repeats: true) { [weak self] _ in
MainActor.assumeIsolated { self?.refresh() }
}
timer.tolerance = 0.1
RunLoop.main.add(timer, forMode: .common)
self.timer = timer
}
controller.requestIfReady(surfaceID: windowID, request: request)
} else {
timer?.invalidate()
timer = nil
}
}
func stop() {
timer?.invalidate()
timer = nil
for observer in observers { NotificationCenter.default.removeObserver(observer) }
observers.removeAll()
for observer in sleepObservers { NSWorkspace.shared.notificationCenter.removeObserver(observer) }
sleepObservers.removeAll()
if let monitor { NSEvent.removeMonitor(monitor) }
monitor = nil
controller.removeSurface(windowID)
}
}