feat: 快速双击 Fn 键切换语音输入/AI 助手模式
This commit is contained in:
parent
847ae72e49
commit
ad72907e89
@ -80,6 +80,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
keyMonitor.onFnUp = { [weak self] in
|
keyMonitor.onFnUp = { [weak self] in
|
||||||
self?.stopRecording()
|
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
|
// MARK: - Recording
|
||||||
|
|||||||
@ -201,6 +201,10 @@ final class MenuBarController: NSObject {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refreshModeMenu() {
|
||||||
|
updateModeMenuState()
|
||||||
|
}
|
||||||
|
|
||||||
private func updateInputLLMMenuState() {
|
private func updateInputLLMMenuState() {
|
||||||
inputLLMEnabledItem.state = Config.shared.inputLLM.enabled ? .on : .off
|
inputLLMEnabledItem.state = Config.shared.inputLLM.enabled ? .on : .off
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,28 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import CoreGraphics
|
import CoreGraphics
|
||||||
|
|
||||||
/// 全局热键监听器:只监听 Fn 键
|
/// 全局热键监听器:只监听 Fn 键(支持双击切换模式)
|
||||||
final class KeyMonitor {
|
final class KeyMonitor {
|
||||||
|
|
||||||
// Fn 键标志
|
// Fn 键标志
|
||||||
private static let fnMask: UInt64 = 0x00800000
|
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 eventTap: CFMachPort?
|
||||||
private var runLoopSource: CFRunLoopSource?
|
private var runLoopSource: CFRunLoopSource?
|
||||||
private var isFnPressed = false
|
private var isFnPressed = false
|
||||||
|
|
||||||
|
// 时间戳追踪
|
||||||
|
private var lastFnUpTime: TimeInterval = 0
|
||||||
|
private var fnDownTime: TimeInterval = 0
|
||||||
|
private var longPressTimer: Timer?
|
||||||
|
|
||||||
var onFnDown: (() -> Void)?
|
var onFnDown: (() -> Void)?
|
||||||
var onFnUp: (() -> Void)?
|
var onFnUp: (() -> Void)?
|
||||||
|
var onFnDoubleClick: (() -> Void)? // 双击 Fn 切换模式
|
||||||
|
|
||||||
func start() -> Bool {
|
func start() -> Bool {
|
||||||
guard eventTap == nil else { return true }
|
guard eventTap == nil else { return true }
|
||||||
@ -86,17 +96,51 @@ final class KeyMonitor {
|
|||||||
|
|
||||||
if fnActive && !isFnPressed {
|
if fnActive && !isFnPressed {
|
||||||
isFnPressed = true
|
isFnPressed = true
|
||||||
print("[KeyMonitor] ⚡️ Fn DOWN")
|
fnDownTime = ProcessInfo.processInfo.systemUptime
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.onFnDown?()
|
// 检测双击:上次松开距现在 < 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
|
return nil
|
||||||
}
|
}
|
||||||
if !fnActive && isFnPressed {
|
if !fnActive && isFnPressed {
|
||||||
isFnPressed = false
|
isFnPressed = false
|
||||||
print("[KeyMonitor] ⚡️ Fn UP")
|
lastFnUpTime = ProcessInfo.processInfo.systemUptime
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.onFnUp?()
|
// 取消长按 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 nil
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user