From ad72907e89c266a7365c6cac22dd9c919ef98eaf Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Fri, 24 Apr 2026 23:34:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BF=AB=E9=80=9F=E5=8F=8C=E5=87=BB=20?= =?UTF-8?q?Fn=20=E9=94=AE=E5=88=87=E6=8D=A2=E8=AF=AD=E9=9F=B3=E8=BE=93?= =?UTF-8?q?=E5=85=A5/AI=20=E5=8A=A9=E6=89=8B=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/App/AppDelegate.swift | 12 ++++++ Sources/App/MenuBarController.swift | 4 ++ Sources/Core/KeyMonitor.swift | 58 +++++++++++++++++++++++++---- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index e279607..d201e67 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -80,6 +80,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate { keyMonitor.onFnUp = { [weak self] in self?.stopRecording() } + keyMonitor.onFnDoubleClick = { [weak self] in + self?.toggleModeViaDoubleClick() + } + } + + // MARK: - Double-Click Mode Switch + + private func toggleModeViaDoubleClick() { + let newMode: TriggerMode = Config.shared.triggerMode == .input ? .assist : .input + Config.shared.triggerMode = newMode + menuBarController.refreshModeMenu() + print("[App] Double-click: mode switched to \(newMode)") } // MARK: - Recording diff --git a/Sources/App/MenuBarController.swift b/Sources/App/MenuBarController.swift index 2e82226..5ad2714 100644 --- a/Sources/App/MenuBarController.swift +++ b/Sources/App/MenuBarController.swift @@ -201,6 +201,10 @@ final class MenuBarController: NSObject { return true } + func refreshModeMenu() { + updateModeMenuState() + } + private func updateInputLLMMenuState() { inputLLMEnabledItem.state = Config.shared.inputLLM.enabled ? .on : .off } diff --git a/Sources/Core/KeyMonitor.swift b/Sources/Core/KeyMonitor.swift index 40e9a94..9d41f08 100644 --- a/Sources/Core/KeyMonitor.swift +++ b/Sources/Core/KeyMonitor.swift @@ -1,18 +1,28 @@ import Cocoa import CoreGraphics -/// 全局热键监听器:只监听 Fn 键 +/// 全局热键监听器:只监听 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 } @@ -86,17 +96,51 @@ final class KeyMonitor { if fnActive && !isFnPressed { isFnPressed = true - print("[KeyMonitor] ⚡️ Fn DOWN") - DispatchQueue.main.async { [weak self] in - self?.onFnDown?() + 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 - print("[KeyMonitor] ⚡️ Fn UP") - DispatchQueue.main.async { [weak self] in - self?.onFnUp?() + 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 }