feat: 菜单栏切换模式,统一使用 Fn 键触发
This commit is contained in:
parent
67b3024bda
commit
a12258e051
@ -12,7 +12,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
private var isRecording = false
|
||||
private var finalTranscription: String = ""
|
||||
private var currentMode: LLMMode = .input
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
NSApp.setActivationPolicy(.accessory)
|
||||
@ -74,23 +73,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
// Fn 键:语音输入模式
|
||||
// Fn 键:根据当前模式执行不同功能
|
||||
keyMonitor.onFnDown = { [weak self] in
|
||||
self?.currentMode = .input
|
||||
self?.startRecording()
|
||||
}
|
||||
keyMonitor.onFnUp = { [weak self] in
|
||||
self?.stopRecording()
|
||||
}
|
||||
|
||||
// Control 键:AI 助手模式
|
||||
keyMonitor.onControlDown = { [weak self] in
|
||||
self?.currentMode = .assist
|
||||
self?.startRecording()
|
||||
}
|
||||
keyMonitor.onControlUp = { [weak self] in
|
||||
self?.stopRecording()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Recording
|
||||
@ -103,7 +92,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
recordingPanel.showAnimated()
|
||||
|
||||
// 根据模式显示不同提示
|
||||
let promptText = currentMode == .input ? "Listening..." : "Ask AI..."
|
||||
let promptText = Config.shared.triggerMode == .input ? "Listening..." : "Ask AI..."
|
||||
recordingPanel.updateText(promptText)
|
||||
|
||||
let language = Config.shared.language.rawValue
|
||||
@ -155,14 +144,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
let config = Config.shared.getLLMConfig(for: currentMode)
|
||||
// 根据 triggerMode 选择配置和逻辑
|
||||
let mode = Config.shared.triggerMode
|
||||
let config = mode == .input ? Config.shared.inputLLM : Config.shared.assistLLM
|
||||
|
||||
switch currentMode {
|
||||
switch mode {
|
||||
case .input:
|
||||
// Fn 键模式:语音输入 + 纠错
|
||||
handleInputMode(text: text, config: config)
|
||||
case .assist:
|
||||
// Control 键模式:AI 助手对话
|
||||
handleAssistMode(text: text, config: config)
|
||||
}
|
||||
|
||||
|
||||
@ -6,37 +6,33 @@ final class MenuBarController: NSObject {
|
||||
private let statusItem: NSStatusItem
|
||||
private let menu = NSMenu()
|
||||
private var languageMenuItems: [NSMenuItem] = []
|
||||
private var modeMenuItems: [NSMenuItem] = []
|
||||
|
||||
// Input LLM (Fn 键 - 语音输入纠错)
|
||||
// LLM 配置
|
||||
private let inputLLMSubmenu = NSMenu()
|
||||
private let inputLLMEnabledItem: NSMenuItem
|
||||
|
||||
// Assist LLM (Control 键 - AI 助手)
|
||||
private let assistLLMSubmenu = NSMenu()
|
||||
private let assistLLMEnabledItem: NSMenuItem
|
||||
|
||||
// 设置窗口
|
||||
private let inputSettingsWindow: SettingsWindow
|
||||
private let assistSettingsWindow: SettingsWindow
|
||||
|
||||
override init() {
|
||||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
|
||||
// Input LLM 启用项
|
||||
inputLLMEnabledItem = NSMenuItem(
|
||||
title: "Enable",
|
||||
action: #selector(toggleInputLLM),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
|
||||
// Assist LLM 启用项
|
||||
assistLLMEnabledItem = NSMenuItem(
|
||||
title: "Enable",
|
||||
action: #selector(toggleAssistLLM),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
|
||||
// 创建两个设置窗口
|
||||
inputSettingsWindow = SettingsWindow(mode: .input)
|
||||
assistSettingsWindow = SettingsWindow(mode: .assist)
|
||||
|
||||
@ -88,7 +84,36 @@ final class MenuBarController: NSObject {
|
||||
|
||||
menu.addItem(.separator())
|
||||
|
||||
// ===== Input LLM (Fn 键 - 语音输入纠错) =====
|
||||
// ===== 模式切换 =====
|
||||
let modeMenu = NSMenu()
|
||||
let inputModeItem = NSMenuItem(
|
||||
title: "🎤 语音输入",
|
||||
action: #selector(selectMode(_:)),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
inputModeItem.representedObject = TriggerMode.input
|
||||
inputModeItem.state = Config.shared.triggerMode == .input ? .on : .off
|
||||
modeMenuItems.append(inputModeItem)
|
||||
modeMenu.addItem(inputModeItem)
|
||||
|
||||
let assistModeItem = NSMenuItem(
|
||||
title: "💬 AI 助手",
|
||||
action: #selector(selectMode(_:)),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
assistModeItem.representedObject = TriggerMode.assist
|
||||
assistModeItem.state = Config.shared.triggerMode == .assist ? .on : .off
|
||||
modeMenuItems.append(assistModeItem)
|
||||
modeMenu.addItem(assistModeItem)
|
||||
|
||||
let modeSubmenuItem = NSMenuItem()
|
||||
modeSubmenuItem.title = "Mode"
|
||||
modeSubmenuItem.submenu = modeMenu
|
||||
menu.addItem(modeSubmenuItem)
|
||||
|
||||
menu.addItem(.separator())
|
||||
|
||||
// ===== Input LLM =====
|
||||
inputLLMSubmenu.addItem(inputLLMEnabledItem)
|
||||
inputLLMEnabledItem.target = self
|
||||
inputLLMEnabledItem.action = #selector(toggleInputLLM)
|
||||
@ -103,13 +128,13 @@ final class MenuBarController: NSObject {
|
||||
inputLLMSubmenu.addItem(inputSettingsItem)
|
||||
|
||||
let inputSubmenuItem = NSMenuItem()
|
||||
inputSubmenuItem.title = "🎤 Input LLM (Fn)"
|
||||
inputSubmenuItem.title = "🎤 Input LLM"
|
||||
inputSubmenuItem.submenu = inputLLMSubmenu
|
||||
menu.addItem(inputSubmenuItem)
|
||||
|
||||
menu.addItem(.separator())
|
||||
|
||||
// ===== Assist LLM (Control 键 - AI 助手) =====
|
||||
// ===== Assist LLM =====
|
||||
assistLLMSubmenu.addItem(assistLLMEnabledItem)
|
||||
assistLLMEnabledItem.target = self
|
||||
assistLLMEnabledItem.action = #selector(toggleAssistLLM)
|
||||
@ -124,7 +149,7 @@ final class MenuBarController: NSObject {
|
||||
assistLLMSubmenu.addItem(assistSettingsItem)
|
||||
|
||||
let assistSubmenuItem = NSMenuItem()
|
||||
assistSubmenuItem.title = "💬 Assist LLM (Control)"
|
||||
assistSubmenuItem.title = "💬 Assist LLM"
|
||||
assistSubmenuItem.submenu = assistLLMSubmenu
|
||||
menu.addItem(assistSubmenuItem)
|
||||
|
||||
@ -150,12 +175,18 @@ final class MenuBarController: NSObject {
|
||||
assistLLMEnabledItem.state = Config.shared.assistLLM.enabled ? .on : .off
|
||||
}
|
||||
|
||||
private func updateModeMenuState() {
|
||||
for item in modeMenuItems {
|
||||
guard let mode = item.representedObject as? TriggerMode else { continue }
|
||||
item.state = mode == Config.shared.triggerMode ? .on : .off
|
||||
}
|
||||
}
|
||||
|
||||
private func updateLanguageMenuState() {
|
||||
for item in languageMenuItems {
|
||||
guard let lang = item.representedObject as? RecognitionLanguage else { continue }
|
||||
item.state = lang == Config.shared.language ? .on : .off
|
||||
}
|
||||
// 更新第一项的显示
|
||||
if let first = menu.items.first {
|
||||
first.title = "Language: \(Config.shared.language.displayName)"
|
||||
}
|
||||
@ -168,6 +199,13 @@ final class MenuBarController: NSObject {
|
||||
print("[Menu] Language switched to: \(lang.displayName)")
|
||||
}
|
||||
|
||||
@objc private func selectMode(_ sender: NSMenuItem) {
|
||||
guard let mode = sender.representedObject as? TriggerMode else { return }
|
||||
Config.shared.triggerMode = mode
|
||||
updateModeMenuState()
|
||||
print("[Menu] Mode switched to: \(mode)")
|
||||
}
|
||||
|
||||
@objc private func toggleInputLLM() {
|
||||
var config = Config.shared.inputLLM
|
||||
config.enabled.toggle()
|
||||
@ -186,15 +224,15 @@ final class MenuBarController: NSObject {
|
||||
|
||||
@objc private func openInputSettings() {
|
||||
inputSettingsWindow.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
@objc private func openAssistSettings() {
|
||||
assistSettingsWindow.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
@objc private func quit() {
|
||||
NSApp.terminate(nil)
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,23 +1,18 @@
|
||||
import Cocoa
|
||||
import CoreGraphics
|
||||
|
||||
/// 全局热键监听器:Fn 键(语音输入)+ Control 键(AI 助手)
|
||||
/// 全局热键监听器:只监听 Fn 键
|
||||
final class KeyMonitor {
|
||||
|
||||
// Fn 键:NX_SECONDARYFNMASK
|
||||
// Fn 键标志
|
||||
private static let fnMask: UInt64 = 0x00800000
|
||||
// Control 键:NX_CONTROLMASK
|
||||
private static let controlMask: UInt64 = 0x00001000
|
||||
|
||||
private var eventTap: CFMachPort?
|
||||
private var runLoopSource: CFRunLoopSource?
|
||||
private var isFnPressed = false
|
||||
private var isControlPressed = false
|
||||
|
||||
var onFnDown: (() -> Void)?
|
||||
var onFnUp: (() -> Void)?
|
||||
var onControlDown: (() -> Void)?
|
||||
var onControlUp: (() -> Void)?
|
||||
|
||||
func start() -> Bool {
|
||||
guard eventTap == nil else { return true }
|
||||
@ -29,8 +24,6 @@ final class KeyMonitor {
|
||||
}
|
||||
|
||||
let eventMask = (1 << CGEventType.flagsChanged.rawValue)
|
||||
| (1 << CGEventType.keyDown.rawValue)
|
||||
| (1 << CGEventType.keyUp.rawValue)
|
||||
|
||||
let callback: CGEventTapCallBack = { (proxy, type, event, refcon) -> Unmanaged<CGEvent>? in
|
||||
let monitor = Unmanaged<KeyMonitor>.fromOpaque(refcon!).takeUnretainedValue()
|
||||
@ -68,7 +61,7 @@ final class KeyMonitor {
|
||||
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
|
||||
CGEvent.tapEnable(tap: tap, enable: true)
|
||||
|
||||
print("[KeyMonitor] ✅ Started (Fn + Control)")
|
||||
print("[KeyMonitor] ✅ Started")
|
||||
return true
|
||||
}
|
||||
|
||||
@ -84,49 +77,30 @@ final class KeyMonitor {
|
||||
}
|
||||
|
||||
private func handleEvent(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
|
||||
guard type == .flagsChanged else {
|
||||
return Unmanaged.passUnretained(event)
|
||||
}
|
||||
|
||||
let flags = event.flags.rawValue
|
||||
_ = event.getIntegerValueField(.keyboardEventKeycode)
|
||||
let fnActive = (flags & Self.fnMask) != 0
|
||||
|
||||
let fnInFlags = (flags & Self.fnMask) != 0
|
||||
let controlInFlags = (flags & Self.controlMask) != 0
|
||||
|
||||
switch type {
|
||||
case .flagsChanged:
|
||||
// Fn 键检测
|
||||
if fnInFlags && !isFnPressed {
|
||||
if fnActive && !isFnPressed {
|
||||
isFnPressed = true
|
||||
print("[KeyMonitor] ⚡️ Fn DOWN")
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.onFnDown?()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !fnInFlags && isFnPressed {
|
||||
if !fnActive && isFnPressed {
|
||||
isFnPressed = false
|
||||
print("[KeyMonitor] ⚡️ Fn UP")
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.onFnUp?()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Control 键检测
|
||||
if controlInFlags && !isControlPressed {
|
||||
isControlPressed = true
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.onControlDown?()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !controlInFlags && isControlPressed {
|
||||
isControlPressed = false
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.onControlUp?()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return Unmanaged.passUnretained(event)
|
||||
|
||||
default:
|
||||
return Unmanaged.passUnretained(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,8 +19,13 @@ enum RecognitionLanguage: String, CaseIterable {
|
||||
}
|
||||
|
||||
enum LLMMode {
|
||||
case input // Fn 键:语音输入纠错
|
||||
case assist // Control 键:AI 助手对话
|
||||
case input // 语音输入纠错
|
||||
case assist // AI 助手对话
|
||||
}
|
||||
|
||||
enum TriggerMode: String {
|
||||
case input = "input" // 语音输入
|
||||
case assist = "assist" // AI 助手
|
||||
}
|
||||
|
||||
struct LLMConfig {
|
||||
@ -43,14 +48,15 @@ final class Config {
|
||||
|
||||
private enum Keys {
|
||||
static let language = "recognition_language"
|
||||
static let triggerMode = "trigger_mode"
|
||||
|
||||
// Fn 键模式(语音输入纠错)
|
||||
// 输入模式 LLM(语音输入纠错)
|
||||
static let inputLLMEnabled = "input_llm_enabled"
|
||||
static let inputLLMBaseURL = "input_llm_base_url"
|
||||
static let inputLLMAPIKey = "input_llm_api_key"
|
||||
static let inputLLMModel = "input_llm_model"
|
||||
|
||||
// Control 键模式(AI 助手)
|
||||
// 助手模式 LLM(AI 助手对话)
|
||||
static let assistLLMEnabled = "assist_llm_enabled"
|
||||
static let assistLLMBaseURL = "assist_llm_base_url"
|
||||
static let assistLLMAPIKey = "assist_llm_api_key"
|
||||
@ -70,7 +76,22 @@ final class Config {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fn 键模式配置(语音输入纠错)
|
||||
// MARK: - 触发模式(菜单栏切换)
|
||||
|
||||
var triggerMode: TriggerMode {
|
||||
get {
|
||||
guard let raw = defaults.string(forKey: Keys.triggerMode),
|
||||
let mode = TriggerMode(rawValue: raw) else {
|
||||
return .input
|
||||
}
|
||||
return mode
|
||||
}
|
||||
set {
|
||||
defaults.set(newValue.rawValue, forKey: Keys.triggerMode)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 输入模式配置(语音输入纠错)
|
||||
|
||||
var inputLLM: LLMConfig {
|
||||
get {
|
||||
|
||||
Binary file not shown.
Loading…
Reference in New Issue
Block a user