151 lines
5.4 KiB
Swift
151 lines
5.4 KiB
Swift
import Cocoa
|
||
import CoreGraphics
|
||
|
||
/// 全局热键监听器:只监听 Fn 键(支持双击切换模式)
|
||
final class KeyMonitor {
|
||
|
||
// Fn 键标志
|
||
private static let fnMask: UInt64 = 0x00800000
|
||
|
||
// 双击检测
|
||
private static let doubleClickWindow: TimeInterval = 0.40 // 两次点击的最大间隔
|
||
private static let longPressThreshold: TimeInterval = 0.20 // 长按判定阈值
|
||
|
||
private var eventTap: CFMachPort?
|
||
private var runLoopSource: CFRunLoopSource?
|
||
private var isFnPressed = false
|
||
|
||
// 时间戳追踪
|
||
private var lastFnUpTime: TimeInterval = 0
|
||
private var fnDownTime: TimeInterval = 0
|
||
private var longPressTimer: Timer?
|
||
|
||
var onFnDown: (() -> Void)?
|
||
var onFnUp: (() -> Void)?
|
||
var onFnDoubleClick: (() -> Void)? // 双击 Fn 切换模式
|
||
|
||
func start() -> Bool {
|
||
guard eventTap == nil else { return true }
|
||
|
||
guard CGPreflightListenEventAccess() else {
|
||
CGRequestListenEventAccess()
|
||
print("[KeyMonitor] ❌ Accessibility permission not granted")
|
||
return false
|
||
}
|
||
|
||
let eventMask = (1 << CGEventType.flagsChanged.rawValue)
|
||
|
||
let callback: CGEventTapCallBack = { (proxy, type, event, refcon) -> Unmanaged<CGEvent>? in
|
||
let monitor = Unmanaged<KeyMonitor>.fromOpaque(refcon!).takeUnretainedValue()
|
||
return monitor.handleEvent(proxy: proxy, type: type, event: event)
|
||
}
|
||
|
||
var tap = CGEvent.tapCreate(
|
||
tap: .cghidEventTap,
|
||
place: .headInsertEventTap,
|
||
options: .defaultTap,
|
||
eventsOfInterest: CGEventMask(eventMask),
|
||
callback: callback,
|
||
userInfo: Unmanaged.passUnretained(self).toOpaque()
|
||
)
|
||
|
||
if tap == nil {
|
||
print("[KeyMonitor] ⚠️ HID tap failed, falling back to session tap")
|
||
tap = CGEvent.tapCreate(
|
||
tap: .cgSessionEventTap,
|
||
place: .headInsertEventTap,
|
||
options: .defaultTap,
|
||
eventsOfInterest: CGEventMask(eventMask),
|
||
callback: callback,
|
||
userInfo: Unmanaged.passUnretained(self).toOpaque()
|
||
)
|
||
}
|
||
|
||
guard let tap = tap else {
|
||
print("[KeyMonitor] ❌ Failed to create event tap")
|
||
return false
|
||
}
|
||
|
||
eventTap = tap
|
||
runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
|
||
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
|
||
CGEvent.tapEnable(tap: tap, enable: true)
|
||
|
||
print("[KeyMonitor] ✅ Started")
|
||
return true
|
||
}
|
||
|
||
func stop() {
|
||
guard let tap = eventTap else { return }
|
||
CGEvent.tapEnable(tap: tap, enable: false)
|
||
if let source = runLoopSource {
|
||
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, .commonModes)
|
||
}
|
||
eventTap = nil
|
||
runLoopSource = nil
|
||
print("[KeyMonitor] Stopped")
|
||
}
|
||
|
||
private func handleEvent(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
|
||
guard type == .flagsChanged else {
|
||
return Unmanaged.passUnretained(event)
|
||
}
|
||
|
||
let flags = event.flags.rawValue
|
||
let fnActive = (flags & Self.fnMask) != 0
|
||
|
||
if fnActive && !isFnPressed {
|
||
isFnPressed = true
|
||
fnDownTime = ProcessInfo.processInfo.systemUptime
|
||
|
||
// 检测双击:上次松开距现在 < 400ms 且之前有单击
|
||
let elapsed = fnDownTime - lastFnUpTime
|
||
if elapsed > 0 && elapsed < Self.doubleClickWindow {
|
||
// 双击!切换模式,取消长按 timer
|
||
longPressTimer?.invalidate()
|
||
longPressTimer = nil
|
||
print("[KeyMonitor] ⚡️ Fn DOUBLE-CLICK → switch mode")
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.onFnDoubleClick?()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 延迟 200ms 判定是否为长按
|
||
print("[KeyMonitor] ⚡️ Fn DOWN (waiting for long-press/double-click)")
|
||
longPressTimer?.invalidate()
|
||
longPressTimer = Timer.scheduledTimer(withTimeInterval: Self.longPressThreshold, repeats: false) { [weak self] _ in
|
||
guard let self = self, self.isFnPressed else { return }
|
||
print("[KeyMonitor] ⚡️ Fn LONG-PRESS → start recording")
|
||
self.longPressTimer = nil
|
||
DispatchQueue.main.async {
|
||
self.onFnDown?()
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
if !fnActive && isFnPressed {
|
||
isFnPressed = false
|
||
lastFnUpTime = ProcessInfo.processInfo.systemUptime
|
||
|
||
// 取消长按 timer(说明是短按/单击)
|
||
longPressTimer?.invalidate()
|
||
longPressTimer = nil
|
||
|
||
print("[KeyMonitor] ⚡️ Fn UP (was pressed for \(String(format: "%.0f", (lastFnUpTime - fnDownTime) * 1000))ms)")
|
||
|
||
// 如果长按 timer 已被触发(已开始录音),正常触发 onFnUp
|
||
// 否则这是短按(单击或双击的第一下),不触发录音
|
||
if lastFnUpTime - fnDownTime >= Self.longPressThreshold {
|
||
// 长按结束,正常触发 onFnUp
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.onFnUp?()
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
return Unmanaged.passUnretained(event)
|
||
}
|
||
}
|