import Cocoa /// 菜单栏控制器:管理状态栏图标和菜单 final class MenuBarController: NSObject { private let statusItem: NSStatusItem private let menu = NSMenu() private var languageMenuItems: [NSMenuItem] = [] private var modeMenuItems: [NSMenuItem] = [] // LLM 配置 private let inputLLMSubmenu = NSMenu() private let inputLLMEnabledItem: NSMenuItem private let assistLLMSubmenu = NSMenu() private let assistLLMEnabledItem: NSMenuItem private let inputSettingsWindow: SettingsWindow private let assistSettingsWindow: SettingsWindow private let agentSettingsWindow: SettingsWindow private let agentLLMEnabledItem: NSMenuItem private let agentVoiceInputItem: NSMenuItem private let agentTextInputItem: NSMenuItem private let memoryWindow: MemoryWindow override init() { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) inputLLMEnabledItem = NSMenuItem( title: "Enable", action: #selector(toggleInputLLM), keyEquivalent: "" ) assistLLMEnabledItem = NSMenuItem( title: "Enable", action: #selector(toggleAssistLLM), keyEquivalent: "" ) inputSettingsWindow = SettingsWindow(mode: .input) assistSettingsWindow = SettingsWindow(mode: .assist) agentSettingsWindow = SettingsWindow(mode: .agent) memoryWindow = MemoryWindow() agentLLMEnabledItem = NSMenuItem( title: "Enable", action: #selector(toggleAgentLLM), keyEquivalent: "" ) agentVoiceInputItem = NSMenuItem( title: "🎤 语音输入", action: #selector(selectAgentVoiceInput), keyEquivalent: "" ) agentTextInputItem = NSMenuItem( title: "⌨️ 打字输入", action: #selector(selectAgentTextInput), keyEquivalent: "" ) super.init() configureStatusItem() buildMenu() } private func configureStatusItem() { if let button = statusItem.button { button.image = NSImage( systemSymbolName: "mic.fill", accessibilityDescription: "Voice Input" ) button.toolTip = "Voice Input — 按住 Fn 键录音" } } private func buildMenu() { // 当前语言显示 let currentLanguageItem = NSMenuItem( title: "Language: \(Config.shared.language.displayName)", action: nil, keyEquivalent: "" ) currentLanguageItem.isEnabled = false menu.addItem(currentLanguageItem) menu.addItem(.separator()) // 语言选择子菜单 let languageMenu = NSMenu() for lang in RecognitionLanguage.allCases { let item = NSMenuItem( title: lang.displayName, action: #selector(selectLanguage(_:)), keyEquivalent: "" ) item.target = self item.representedObject = lang item.state = lang == Config.shared.language ? .on : .off languageMenu.addItem(item) languageMenuItems.append(item) } // 禁用 autoenablesItems,防止系统自动禁用菜单项 languageMenu.autoenablesItems = false let languageSubmenuItem = NSMenuItem() languageSubmenuItem.title = "Language" languageSubmenuItem.submenu = languageMenu menu.addItem(languageSubmenuItem) // 调试:打印每个语言菜单项的状态 for item in languageMenuItems { print("[Menu Debug] Language item: '\(item.title)' enabled=\(item.isEnabled) target=\(String(describing: item.target)) action=\(String(describing: item.action))") } menu.addItem(.separator()) // ===== 模式切换 ===== let modeMenu = NSMenu() let inputModeItem = NSMenuItem( title: "🎤 语音输入", action: #selector(selectMode(_:)), keyEquivalent: "" ) inputModeItem.target = self 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.target = self assistModeItem.representedObject = TriggerMode.assist assistModeItem.state = Config.shared.triggerMode == .assist ? .on : .off modeMenuItems.append(assistModeItem) modeMenu.addItem(assistModeItem) let agentModeItem = NSMenuItem( title: "🤖 智能体", action: #selector(selectMode(_:)), keyEquivalent: "" ) agentModeItem.target = self agentModeItem.representedObject = TriggerMode.agent agentModeItem.state = Config.shared.triggerMode == .agent ? .on : .off modeMenuItems.append(agentModeItem) modeMenu.addItem(agentModeItem) // 禁用 autoenablesItems,防止系统自动禁用菜单项 modeMenu.autoenablesItems = false let modeSubmenuItem = NSMenuItem() modeSubmenuItem.title = "Mode" modeSubmenuItem.submenu = modeMenu menu.addItem(modeSubmenuItem) // 调试:打印每个模式菜单项的状态 for item in modeMenuItems { print("[Menu Debug] Mode item: '\(item.title)' enabled=\(item.isEnabled) target=\(String(describing: item.target)) action=\(String(describing: item.action))") } menu.addItem(.separator()) // ===== Input LLM ===== inputLLMSubmenu.addItem(inputLLMEnabledItem) inputLLMEnabledItem.target = self inputLLMEnabledItem.action = #selector(toggleInputLLM) updateInputLLMMenuState() let inputSettingsItem = NSMenuItem( title: "Settings...", action: #selector(openInputSettings), keyEquivalent: "" ) inputSettingsItem.target = self inputLLMSubmenu.addItem(inputSettingsItem) let inputSubmenuItem = NSMenuItem() inputSubmenuItem.title = "🎤 Input LLM" inputSubmenuItem.submenu = inputLLMSubmenu menu.addItem(inputSubmenuItem) menu.addItem(.separator()) // ===== Assist LLM ===== assistLLMSubmenu.addItem(assistLLMEnabledItem) assistLLMEnabledItem.target = self assistLLMEnabledItem.action = #selector(toggleAssistLLM) updateAssistLLMMenuState() let assistSettingsItem = NSMenuItem( title: "Settings...", action: #selector(openAssistSettings), keyEquivalent: "" ) assistSettingsItem.target = self assistLLMSubmenu.addItem(assistSettingsItem) let assistSubmenuItem = NSMenuItem() assistSubmenuItem.title = "💬 Assist LLM" assistSubmenuItem.submenu = assistLLMSubmenu menu.addItem(assistSubmenuItem) menu.addItem(.separator()) // ===== Agent LLM ===== let agentLLMSubmenu = NSMenu() agentLLMEnabledItem.target = self agentLLMSubmenu.addItem(agentLLMEnabledItem) updateAgentLLMMenuState() agentLLMSubmenu.addItem(.separator()) agentVoiceInputItem.target = self agentVoiceInputItem.state = Config.shared.agentInputMode == "voice" ? .on : .off agentLLMSubmenu.addItem(agentVoiceInputItem) agentTextInputItem.target = self agentTextInputItem.state = Config.shared.agentInputMode == "text" ? .on : .off agentLLMSubmenu.addItem(agentTextInputItem) agentLLMSubmenu.addItem(.separator()) let agentSettingsItem = NSMenuItem( title: "Settings...", action: #selector(openAgentSettings), keyEquivalent: "" ) agentSettingsItem.target = self agentSettingsItem.isEnabled = true agentLLMSubmenu.addItem(agentSettingsItem) let agentSubmenuItem = NSMenuItem() agentSubmenuItem.title = "🤖 Agent LLM" agentSubmenuItem.submenu = agentLLMSubmenu menu.addItem(agentSubmenuItem) menu.addItem(.separator()) // 助手设置 let assistantSettingsItem = NSMenuItem( title: "⚙️ 助手设置...", action: #selector(openAgentSettings), keyEquivalent: "" ) assistantSettingsItem.target = self menu.addItem(assistantSettingsItem) // 记忆管理 let memoryItem = NSMenuItem( title: "🧠 记忆管理...", action: #selector(openMemoryManager), keyEquivalent: "" ) memoryItem.target = self menu.addItem(memoryItem) menu.addItem(.separator()) // 退出 let quitItem = NSMenuItem( title: "Quit", action: #selector(quit), keyEquivalent: "q" ) quitItem.target = self menu.addItem(quitItem) // 关闭所有菜单的自动验证,改为手动管理 menu.autoenablesItems = false languageMenu.autoenablesItems = false modeMenu.autoenablesItems = false inputLLMSubmenu.autoenablesItems = false assistLLMSubmenu.autoenablesItems = false agentLLMSubmenu.autoenablesItems = false print("[Menu Debug] Agent Settings item: title=\(agentSettingsItem.title) enabled=\(agentSettingsItem.isEnabled) target=\(String(describing: agentSettingsItem.target)) action=\(String(describing: agentSettingsItem.action)) submenu=\(String(describing: agentSettingsItem.menu))") statusItem.menu = menu } // MARK: - NSMenuItemValidation /// 手动验证所有菜单项(替代系统的自动验证) @objc func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { // 所有菜单项统一启用,由 target/action 决定是否响应 return true } func refreshModeMenu() { updateModeMenuState() } private func updateInputLLMMenuState() { inputLLMEnabledItem.state = Config.shared.inputLLM.enabled ? .on : .off } private func updateAssistLLMMenuState() { assistLLMEnabledItem.state = Config.shared.assistLLM.enabled ? .on : .off } private func updateAgentLLMMenuState() { let modelsFile = Config.shared.loadAgentModels() let validModels = modelsFile.models.filter { !$0.url.isEmpty && !$0.name.isEmpty && !$0.apikey.isEmpty && !$0.modes.isEmpty } agentLLMEnabledItem.state = validModels.isEmpty ? .off : .on } 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)" } } @objc private func selectLanguage(_ sender: NSMenuItem) { print("[Menu Debug] selectLanguage called! sender.title=\(sender.title) sender.representedObject=\(String(describing: sender.representedObject))") guard let lang = sender.representedObject as? RecognitionLanguage else { print("[Menu Debug] selectLanguage FAILED: representedObject is not RecognitionLanguage, actual type: \(type(of: sender.representedObject))") return } Config.shared.language = lang updateLanguageMenuState() print("[Menu] Language switched to: \(lang.displayName)") } @objc private func selectMode(_ sender: NSMenuItem) { print("[Menu Debug] selectMode called! sender.title=\(sender.title) sender.representedObject=\(String(describing: sender.representedObject))") guard let mode = sender.representedObject as? TriggerMode else { print("[Menu Debug] selectMode FAILED: representedObject is not TriggerMode, actual type: \(type(of: sender.representedObject))") return } Config.shared.triggerMode = mode updateModeMenuState() print("[Menu] Mode switched to: \(mode)") } @objc private func toggleInputLLM() { var config = Config.shared.inputLLM config.enabled.toggle() Config.shared.inputLLM = config updateInputLLMMenuState() print("[Menu] Input LLM: \(config.enabled ? "ON" : "OFF")") } @objc private func toggleAssistLLM() { var config = Config.shared.assistLLM config.enabled.toggle() Config.shared.assistLLM = config updateAssistLLMMenuState() print("[Menu] Assist LLM: \(config.enabled ? "ON" : "OFF")") } @objc private func toggleAgentLLM() { let modelsFile = Config.shared.loadAgentModels() let validModels = modelsFile.models.filter { !$0.url.isEmpty && !$0.name.isEmpty && !$0.apikey.isEmpty && !$0.modes.isEmpty } // 只要有有效模型就视为已配置,点击时打开 Settings if validModels.isEmpty { openAgentSettings() } } @objc private func openInputSettings() { showWindowWithDock(inputSettingsWindow) } @objc private func openAssistSettings() { showWindowWithDock(assistSettingsWindow) } @objc private func openAgentSettings() { print("[MenuBar] openAgentSettings called") agentSettingsWindow.refreshAgentModelTable() showWindowWithDock(agentSettingsWindow) } @objc private func openMemoryManager() { showWindowWithDock(memoryWindow) } @objc private func selectAgentVoiceInput() { Config.shared.agentInputMode = "voice" agentVoiceInputItem.state = .on agentTextInputItem.state = .off } @objc private func selectAgentTextInput() { Config.shared.agentInputMode = "text" agentVoiceInputItem.state = .off agentTextInputItem.state = .on } /// 显示窗口并临时启用 Dock 图标 private var dockVisibleWindowCount = 0 private func showWindowWithDock(_ window: NSWindow) { dockVisibleWindowCount += 1 if NSApp.activationPolicy() != .regular { NSApp.setActivationPolicy(.regular) } window.delegate = self window.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) } @objc private func quit() { NSApplication.shared.terminate(nil) } } // MARK: - NSWindowDelegate(Dock 回收) extension MenuBarController: NSWindowDelegate { func windowWillClose(_ notification: Notification) { dockVisibleWindowCount -= 1 if dockVisibleWindowCount <= 0 { dockVisibleWindowCount = 0 NSApp.setActivationPolicy(.accessory) } } }