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