Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

281 lines
8.6 KiB
Swift

import AppKit
import CoreGraphics
import Foundation
import OSLog
struct MouseEnhancerSessionState: Equatable, Sendable {
var scrollTapInstalled: Bool
var gestureTapInstalled: Bool
static let inactive = MouseEnhancerSessionState(
scrollTapInstalled: false,
gestureTapInstalled: false
)
}
@MainActor
protocol MouseEnhancerSessionManaging: AnyObject {
var state: MouseEnhancerSessionState { get }
@discardableResult
func activate(configuration: MouseEnhancerConfiguration) -> Bool
func update(configuration: MouseEnhancerConfiguration)
func deactivate()
}
final class MouseEnhancerSession: MouseEnhancerSessionManaging, @unchecked Sendable {
private enum Timing {
static let wakeRestartDelay: TimeInterval = 2
}
private static let gestureEventType = CGEventType(rawValue: UInt32(NSEvent.EventType.gesture.rawValue))!
private static weak var activeSession: MouseEnhancerSession?
private let processor: MouseScrollEventProcessor
private var scrollTap: CFMachPort?
private var scrollRunLoopSource: CFRunLoopSource?
private var gestureTap: CFMachPort?
private var gestureRunLoopSource: CFRunLoopSource?
private var wakeObserver: (any NSObjectProtocol)?
private var restartWorkItem: DispatchWorkItem?
private let logger = Logger(
subsystem: Bundle.main.bundleIdentifier ?? "cc.ggbond.mactools",
category: "MouseEnhancerSession"
)
var state: MouseEnhancerSessionState {
MouseEnhancerSessionState(
scrollTapInstalled: scrollTap != nil,
gestureTapInstalled: gestureTap != nil
)
}
init(configuration: MouseEnhancerConfiguration = .default) {
self.processor = MouseScrollEventProcessor(configuration: configuration)
}
@discardableResult
func activate(configuration: MouseEnhancerConfiguration) -> Bool {
Self.activeSession?.deactivate()
Self.activeSession = self
processor.configuration = configuration
start()
return scrollTap != nil
}
func update(configuration: MouseEnhancerConfiguration) {
processor.configuration = configuration
}
func deactivate() {
if Self.activeSession === self {
Self.activeSession = nil
}
stop()
}
private func start() {
guard scrollTap == nil else {
return
}
processor.resetClassificationState()
startGestureTap()
startScrollTap()
guard scrollTap != nil else {
stopGestureTap()
removeSystemWakeObserver()
logger.error("scroll reverser session failed to start")
return
}
observeSystemWake()
logger.info(
"scroll reverser session started scrollTap=\(self.scrollTap != nil, privacy: .public) gestureTap=\(self.gestureTap != nil, privacy: .public)"
)
}
private func stop() {
cancelPendingRestart()
removeSystemWakeObserver()
stopScrollTap()
stopGestureTap()
logger.info("scroll reverser session stopped")
}
private func startScrollTap() {
let mask = CGEventMask(1) << UInt64(CGEventType.scrollWheel.rawValue)
guard let tap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .tailAppendEventTap,
options: .defaultTap,
eventsOfInterest: mask,
callback: Self.eventTapCallback,
userInfo: Unmanaged.passUnretained(self).toOpaque()
) else {
logger.error("failed to create scroll CGEvent tap; check Accessibility permission")
return
}
guard let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) else {
CFMachPortInvalidate(tap)
logger.error("failed to create scroll CGEvent run loop source")
return
}
CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes)
CGEvent.tapEnable(tap: tap, enable: true)
scrollTap = tap
scrollRunLoopSource = source
}
private func stopScrollTap() {
if let scrollTap {
CGEvent.tapEnable(tap: scrollTap, enable: false)
}
if let scrollRunLoopSource {
CFRunLoopRemoveSource(CFRunLoopGetMain(), scrollRunLoopSource, .commonModes)
}
if let scrollTap {
CFMachPortInvalidate(scrollTap)
}
scrollRunLoopSource = nil
scrollTap = nil
}
private func startGestureTap() {
let mask = CGEventMask(1) << UInt64(Self.gestureEventType.rawValue)
guard let tap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .tailAppendEventTap,
options: .listenOnly,
eventsOfInterest: mask,
callback: Self.eventTapCallback,
userInfo: Unmanaged.passUnretained(self).toOpaque()
) else {
logger.warning("failed to create gesture CGEvent tap; Input Monitoring may be missing")
return
}
guard let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) else {
CFMachPortInvalidate(tap)
logger.warning("failed to create gesture CGEvent run loop source")
return
}
CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes)
CGEvent.tapEnable(tap: tap, enable: true)
gestureTap = tap
gestureRunLoopSource = source
processor.setGestureMonitoringAvailable(true)
}
private func stopGestureTap() {
if let gestureTap {
CGEvent.tapEnable(tap: gestureTap, enable: false)
}
if let gestureRunLoopSource {
CFRunLoopRemoveSource(CFRunLoopGetMain(), gestureRunLoopSource, .commonModes)
}
if let gestureTap {
CFMachPortInvalidate(gestureTap)
}
gestureRunLoopSource = nil
gestureTap = nil
processor.setGestureMonitoringAvailable(false)
}
private func restartTaps() {
stopScrollTap()
stopGestureTap()
processor.resetClassificationState()
startGestureTap()
startScrollTap()
guard scrollTap != nil else {
stopGestureTap()
logger.error("scroll reverser session failed to restart")
return
}
}
private func enableTapsIfNeeded() {
if let scrollTap, !CGEvent.tapIsEnabled(tap: scrollTap) {
CGEvent.tapEnable(tap: scrollTap, enable: true)
}
if let gestureTap, !CGEvent.tapIsEnabled(tap: gestureTap) {
CGEvent.tapEnable(tap: gestureTap, enable: true)
}
}
private func observeSystemWake() {
guard wakeObserver == nil else {
return
}
wakeObserver = NSWorkspace.shared.notificationCenter.addObserver(
forName: NSWorkspace.didWakeNotification,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor [weak self] in
self?.scheduleRestart(after: Timing.wakeRestartDelay)
}
}
}
private func removeSystemWakeObserver() {
if let wakeObserver {
NSWorkspace.shared.notificationCenter.removeObserver(wakeObserver)
}
wakeObserver = nil
}
private func scheduleRestart(after delay: TimeInterval) {
cancelPendingRestart()
let workItem = DispatchWorkItem { [weak self] in
self?.restartWorkItem = nil
self?.restartTaps()
}
restartWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
}
private func cancelPendingRestart() {
restartWorkItem?.cancel()
restartWorkItem = nil
}
private static let eventTapCallback: CGEventTapCallBack = { _, type, event, userInfo in
guard let userInfo else {
return Unmanaged.passUnretained(event)
}
let session = Unmanaged<MouseEnhancerSession>.fromOpaque(userInfo).takeUnretainedValue()
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
session.enableTapsIfNeeded()
return Unmanaged.passUnretained(event)
}
if type == MouseEnhancerSession.gestureEventType {
let touching = NSEvent(cgEvent: event)?.touches(matching: .touching, in: nil).count ?? 0
session.processor.recordGestureTouchingCount(touching)
return Unmanaged.passUnretained(event)
}
if type == .scrollWheel {
session.processor.process(event: event)
}
return Unmanaged.passUnretained(event)
}
}