From 8425fb4a043252e6579732ec5aa873ef460801a3 Mon Sep 17 00:00:00 2001 From: impressionyang Date: Thu, 11 Jun 2026 16:01:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=98=E7=9B=98?= =?UTF-8?q?=E5=9B=BE=E6=A0=87/=E9=80=80=E5=87=BA/=E5=8D=A1=E9=A1=BF=204=20?= =?UTF-8?q?=E4=B8=AA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 关闭窗口无法退出: closeEvent 改为调用 doExit(),正常执行退出流程 2. 托盘右键无法退出: 统一使用 doExit() → qApp->quit(),与菜单/快捷键退出一致 3. 托盘图标更新慢: 6 个状态图标在 setupTrayIcon() 中预创建缓存, updateTrayIcon() 只做 QMap 查找 + setIcon(),避免每次重建 QPixmap 4. 按键识别卡顿/误释放: simulateCapsLock() 在 XTest 模拟前后设置 CapsLockVoiceHotkey::ignoreEvents_ 标志,屏蔽 portal 的 Activated/Deactivated 回传信号,防止状态机被打断 Co-Authored-By: Claude Opus 4.7 --- src/core/caps_lock_voice_hotkey.cpp | 4 +- src/core/caps_lock_voice_hotkey.h | 4 ++ src/core/voice_input_service.cpp | 13 +++- src/ui/main_window.cpp | 92 +++++++++++++---------------- src/ui/main_window.h | 4 ++ 5 files changed, 62 insertions(+), 55 deletions(-) diff --git a/src/core/caps_lock_voice_hotkey.cpp b/src/core/caps_lock_voice_hotkey.cpp index ff2ea2f..0f8a2c5 100644 --- a/src/core/caps_lock_voice_hotkey.cpp +++ b/src/core/caps_lock_voice_hotkey.cpp @@ -206,14 +206,14 @@ void CapsLockVoiceHotkey::handleBindResponse(uint response, const QVariantMap&) } void CapsLockVoiceHotkey::handleActivated(const QString& shortcutId) { - if (!active_) return; + if (!active_ || ignoreEvents_) return; LOG_DEBUG(kTag, QString("快捷键按下: %1").arg(shortcutId)); recording_ = true; emit recordingStarted(); } void CapsLockVoiceHotkey::handleDeactivated(const QString& shortcutId) { - if (!active_) return; + if (!active_ || ignoreEvents_) return; LOG_DEBUG(kTag, QString("快捷键松开: %1").arg(shortcutId)); recording_ = false; emit recordingStopped(); diff --git a/src/core/caps_lock_voice_hotkey.h b/src/core/caps_lock_voice_hotkey.h index f585825..796db1b 100644 --- a/src/core/caps_lock_voice_hotkey.h +++ b/src/core/caps_lock_voice_hotkey.h @@ -37,6 +37,9 @@ public: /** @brief 当前是否正在录音(CapsLock 长按超过 1s 后) */ bool isRecording() const { return recording_; } + /** @brief 临时忽略 portal 信号(XTest 模拟按键期间) */ + void setIgnoreEvents(bool ignore) { ignoreEvents_ = ignore; } + signals: /** @brief 开始录音(长按超过 1 秒后) */ void recordingStarted(); @@ -55,6 +58,7 @@ private: std::unique_ptr impl_; bool active_ = false; bool recording_ = false; + bool ignoreEvents_ = false; void handleSessionResponse(uint response, const QVariantMap& results); void handleBindResponse(uint response, const QVariantMap& results); diff --git a/src/core/voice_input_service.cpp b/src/core/voice_input_service.cpp index 1be41f7..f3fd1e0 100644 --- a/src/core/voice_input_service.cpp +++ b/src/core/voice_input_service.cpp @@ -184,7 +184,6 @@ void VoiceInputService::onHotkeyDeactivated() { simulateCapsLock(); state_ = Idle; LOG_DEBUG(kTag, "短按,恢复 CapsLock 灯"); - emit statusChanged("短按:切换 CapsLock"); } else if (state_ == Recording) { // 长按后松开 → 先恢复 CapsLock,再开始识别 simulateCapsLock(); @@ -254,12 +253,24 @@ void VoiceInputService::onRecognitionComplete(const QString& text) { } void VoiceInputService::simulateCapsLock() { +#ifndef PLATFORM_WINDOWS + // XTest 模拟的按键会被 D-Bus portal 再次捕获,导致 Activated/Deactivated 信号。 + // 在模拟期间屏蔽 portal 信号,防止状态机被打断。 + if (impl_->hotkey) { + impl_->hotkey->setIgnoreEvents(true); + } +#endif if (impl_->injector && impl_->injector->isInitialized()) { impl_->injector->simulateKeysym(0xffe5); LOG_DEBUG(kTag, "模拟 CapsLock 按键"); } else { LOG_WARNING(kTag, "文本注入器未初始化,无法模拟 CapsLock"); } +#ifndef PLATFORM_WINDOWS + if (impl_->hotkey) { + impl_->hotkey->setIgnoreEvents(false); + } +#endif } } // namespace impress diff --git a/src/ui/main_window.cpp b/src/ui/main_window.cpp index e588157..89901ee 100644 --- a/src/ui/main_window.cpp +++ b/src/ui/main_window.cpp @@ -45,7 +45,6 @@ MainWindow::MainWindow(ConfigManager* configManager, connect(voiceInputService_, &VoiceInputService::statusChanged, this, [this](const QString& status) { LOG_DEBUG(kTag, QString("语音输入状态: %1").arg(status)); - updateTrayIcon(status); }); connect(voiceInputService_, &VoiceInputService::error, this, [this](const QString& err) { @@ -111,8 +110,20 @@ void MainWindow::setupTrayIcon() { return; } - // 防止关闭主窗口时应用退出,保持托盘图标运行 - qApp->setQuitOnLastWindowClosed(false); + // 缓存 6 个状态图标,避免每次重建导致 GUI 卡顿 + QIcon recordingIcon(createTrayIcon(QColor("#e74c3c"))); + QIcon recognizingIcon(createTrayIcon(QColor("#f39c12"))); + QIcon waitingIcon(createTrayIcon(QColor("#f1c40f"))); + QIcon readyIcon(createTrayIcon(QColor("#27ae60"))); + QIcon stoppedIcon(createTrayIcon(QColor("#95a5a6"))); + QIcon otherIcon(createTrayIcon(QColor("#3498db"))); + + trayIcons_["recording"] = recordingIcon; + trayIcons_["recognizing"] = recognizingIcon; + trayIcons_["waiting"] = waitingIcon; + trayIcons_["ready"] = readyIcon; + trayIcons_["stopped"] = stoppedIcon; + trayIcons_["other"] = otherIcon; trayMenu_ = new QMenu(this); auto* showAction = trayMenu_->addAction("显示主窗口"); @@ -124,17 +135,13 @@ void MainWindow::setupTrayIcon() { trayMenu_->addSeparator(); auto* exitAction = trayMenu_->addAction("退出"); connect(exitAction, &QAction::triggered, this, [this]() { - if (voiceInputService_) { - voiceInputService_->stop(); - } - qApp->quit(); + doExit(); }); trayIcon_ = new QSystemTrayIcon(this); trayIcon_->setContextMenu(trayMenu_); - - // 先设置图标,再显示托盘 - updateTrayIcon("语音输入就绪"); + trayIcon_->setIcon(readyIcon); + trayIcon_->setToolTip("Impress Voice Input - 语音输入就绪"); trayIcon_->show(); // 双击托盘显示窗口 @@ -146,51 +153,33 @@ void MainWindow::setupTrayIcon() { } }); - // 发送通知气泡,让用户注意到托盘图标 - trayIcon_->showMessage( - "Impress Voice Input", - "语音输入已就绪,CapsLock 快捷键已注册", - QSystemTrayIcon::Information, - 3000 - ); - - LOG_INFO(kTag, QString("系统托盘图标已创建 (可用: %1, 已设置 quitOnLastWindowClosed=false)") - .arg(QSystemTrayIcon::isSystemTrayAvailable())); + LOG_INFO(kTag, "系统托盘图标已创建"); } void MainWindow::updateTrayIcon(const QString& status) { - if (!trayIcon_) return; + if (!trayIcon_ || trayIcons_.isEmpty()) return; - QColor color; - - // 根据状态文字匹配图标颜色 + // 状态映射到缓存图标 + const QIcon* icon = nullptr; if (status.contains("正在录音")) { - color = QColor("#e74c3c"); // 红色 - 录音中 + icon = &trayIcons_["recording"]; } else if (status.contains("正在识别")) { - color = QColor("#f39c12"); // 橙色 - 识别中 + icon = &trayIcons_["recognizing"]; } else if (status.contains("等待长按") || status.contains("PreRecording")) { - color = QColor("#f1c40f"); // 黄色 - 预录音 + icon = &trayIcons_["waiting"]; } else if (status.contains("已启动") || status.contains("就绪")) { - color = QColor("#27ae60"); // 绿色 - 就绪 + icon = &trayIcons_["ready"]; } else if (status.contains("已关闭") || status.contains("停止")) { - color = QColor("#95a5a6"); // 灰色 - 停止 + icon = &trayIcons_["stopped"]; } else { - color = QColor("#3498db"); // 蓝色 - 其他 + icon = &trayIcons_["other"]; } - QPixmap pm = createTrayIcon(color); - QIcon icon(pm); - if (icon.isNull()) { - LOG_ERROR(kTag, "托盘图标创建失败"); - return; - } - - trayIcon_->setIcon(icon); + trayIcon_->setIcon(*icon); trayIcon_->setToolTip(QString("Impress Voice Input - %1").arg(status)); } QPixmap MainWindow::createTrayIcon(const QColor& color) { - // 使用 16x16 标准托盘图标尺寸 const int size = 16; QImage image(size, size, QImage::Format_ARGB32); image.fill(Qt::transparent); @@ -198,7 +187,6 @@ QPixmap MainWindow::createTrayIcon(const QColor& color) { QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing); - // 绘制实心彩色圆 painter.setBrush(color); painter.setPen(Qt::NoPen); int margin = 1; @@ -219,13 +207,7 @@ void MainWindow::setupMenuBar() { auto* exitAction = fileMenu->addAction("退出"); exitAction->setShortcut(QKeySequence("Ctrl+Q")); connect(exitAction, &QAction::triggered, this, [this]() { - if (trayIcon_) { - trayIcon_->hide(); - } - if (voiceInputService_) { - voiceInputService_->stop(); - } - qApp->quit(); + doExit(); }); // 帮助菜单 @@ -249,14 +231,20 @@ void MainWindow::loadStyleSheet() { } void MainWindow::closeEvent(QCloseEvent* event) { - if (trayIcon_) { - trayIcon_->hide(); - } + LOG_INFO(kTag, "主窗口关闭"); + doExit(); + QMainWindow::closeEvent(event); +} + +void MainWindow::doExit() { + LOG_INFO(kTag, "应用退出"); if (voiceInputService_) { voiceInputService_->stop(); } - LOG_INFO(kTag, "主窗口关闭 (应用保持运行,托盘图标可见)"); - event->ignore(); // 不关闭应用,只隐藏窗口 + if (trayIcon_) { + trayIcon_->hide(); + } + qApp->quit(); } void MainWindow::updateModelStatus() { diff --git a/src/ui/main_window.h b/src/ui/main_window.h index 13ac36a..37f1617 100644 --- a/src/ui/main_window.h +++ b/src/ui/main_window.h @@ -2,11 +2,13 @@ #include #include +#include #include class QLabel; class QSystemTrayIcon; class QMenu; +class QIcon; namespace impress { @@ -44,6 +46,7 @@ private: void loadStyleSheet(); void onVoiceInputConfigChanged(); void updateModelStatus(); + void doExit(); ConfigManager* configManager_; SenseVoiceEngine* sttEngine_; @@ -55,6 +58,7 @@ private: QLabel* modelStatusLabel_; QSystemTrayIcon* trayIcon_ = nullptr; QMenu* trayMenu_ = nullptr; + QMap trayIcons_; }; } // namespace impress