fix: 修复托盘图标/退出/卡顿 4 个问题

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 <noreply@anthropic.com>
This commit is contained in:
impressionyang 2026-06-11 16:01:00 +08:00
parent 6bf22041f8
commit 8425fb4a04
5 changed files with 62 additions and 55 deletions

View File

@ -206,14 +206,14 @@ void CapsLockVoiceHotkey::handleBindResponse(uint response, const QVariantMap&)
} }
void CapsLockVoiceHotkey::handleActivated(const QString& shortcutId) { void CapsLockVoiceHotkey::handleActivated(const QString& shortcutId) {
if (!active_) return; if (!active_ || ignoreEvents_) return;
LOG_DEBUG(kTag, QString("快捷键按下: %1").arg(shortcutId)); LOG_DEBUG(kTag, QString("快捷键按下: %1").arg(shortcutId));
recording_ = true; recording_ = true;
emit recordingStarted(); emit recordingStarted();
} }
void CapsLockVoiceHotkey::handleDeactivated(const QString& shortcutId) { void CapsLockVoiceHotkey::handleDeactivated(const QString& shortcutId) {
if (!active_) return; if (!active_ || ignoreEvents_) return;
LOG_DEBUG(kTag, QString("快捷键松开: %1").arg(shortcutId)); LOG_DEBUG(kTag, QString("快捷键松开: %1").arg(shortcutId));
recording_ = false; recording_ = false;
emit recordingStopped(); emit recordingStopped();

View File

@ -37,6 +37,9 @@ public:
/** @brief 当前是否正在录音CapsLock 长按超过 1s 后) */ /** @brief 当前是否正在录音CapsLock 长按超过 1s 后) */
bool isRecording() const { return recording_; } bool isRecording() const { return recording_; }
/** @brief 临时忽略 portal 信号XTest 模拟按键期间) */
void setIgnoreEvents(bool ignore) { ignoreEvents_ = ignore; }
signals: signals:
/** @brief 开始录音(长按超过 1 秒后) */ /** @brief 开始录音(长按超过 1 秒后) */
void recordingStarted(); void recordingStarted();
@ -55,6 +58,7 @@ private:
std::unique_ptr<Impl> impl_; std::unique_ptr<Impl> impl_;
bool active_ = false; bool active_ = false;
bool recording_ = false; bool recording_ = false;
bool ignoreEvents_ = false;
void handleSessionResponse(uint response, const QVariantMap& results); void handleSessionResponse(uint response, const QVariantMap& results);
void handleBindResponse(uint response, const QVariantMap& results); void handleBindResponse(uint response, const QVariantMap& results);

View File

@ -184,7 +184,6 @@ void VoiceInputService::onHotkeyDeactivated() {
simulateCapsLock(); simulateCapsLock();
state_ = Idle; state_ = Idle;
LOG_DEBUG(kTag, "短按,恢复 CapsLock 灯"); LOG_DEBUG(kTag, "短按,恢复 CapsLock 灯");
emit statusChanged("短按:切换 CapsLock");
} else if (state_ == Recording) { } else if (state_ == Recording) {
// 长按后松开 → 先恢复 CapsLock再开始识别 // 长按后松开 → 先恢复 CapsLock再开始识别
simulateCapsLock(); simulateCapsLock();
@ -254,12 +253,24 @@ void VoiceInputService::onRecognitionComplete(const QString& text) {
} }
void VoiceInputService::simulateCapsLock() { 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()) { if (impl_->injector && impl_->injector->isInitialized()) {
impl_->injector->simulateKeysym(0xffe5); impl_->injector->simulateKeysym(0xffe5);
LOG_DEBUG(kTag, "模拟 CapsLock 按键"); LOG_DEBUG(kTag, "模拟 CapsLock 按键");
} else { } else {
LOG_WARNING(kTag, "文本注入器未初始化,无法模拟 CapsLock"); LOG_WARNING(kTag, "文本注入器未初始化,无法模拟 CapsLock");
} }
#ifndef PLATFORM_WINDOWS
if (impl_->hotkey) {
impl_->hotkey->setIgnoreEvents(false);
}
#endif
} }
} // namespace impress } // namespace impress

View File

@ -45,7 +45,6 @@ MainWindow::MainWindow(ConfigManager* configManager,
connect(voiceInputService_, &VoiceInputService::statusChanged, connect(voiceInputService_, &VoiceInputService::statusChanged,
this, [this](const QString& status) { this, [this](const QString& status) {
LOG_DEBUG(kTag, QString("语音输入状态: %1").arg(status)); LOG_DEBUG(kTag, QString("语音输入状态: %1").arg(status));
updateTrayIcon(status);
}); });
connect(voiceInputService_, &VoiceInputService::error, connect(voiceInputService_, &VoiceInputService::error,
this, [this](const QString& err) { this, [this](const QString& err) {
@ -111,8 +110,20 @@ void MainWindow::setupTrayIcon() {
return; return;
} }
// 防止关闭主窗口时应用退出,保持托盘图标运行 // 缓存 6 个状态图标,避免每次重建导致 GUI 卡顿
qApp->setQuitOnLastWindowClosed(false); 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); trayMenu_ = new QMenu(this);
auto* showAction = trayMenu_->addAction("显示主窗口"); auto* showAction = trayMenu_->addAction("显示主窗口");
@ -124,17 +135,13 @@ void MainWindow::setupTrayIcon() {
trayMenu_->addSeparator(); trayMenu_->addSeparator();
auto* exitAction = trayMenu_->addAction("退出"); auto* exitAction = trayMenu_->addAction("退出");
connect(exitAction, &QAction::triggered, this, [this]() { connect(exitAction, &QAction::triggered, this, [this]() {
if (voiceInputService_) { doExit();
voiceInputService_->stop();
}
qApp->quit();
}); });
trayIcon_ = new QSystemTrayIcon(this); trayIcon_ = new QSystemTrayIcon(this);
trayIcon_->setContextMenu(trayMenu_); trayIcon_->setContextMenu(trayMenu_);
trayIcon_->setIcon(readyIcon);
// 先设置图标,再显示托盘 trayIcon_->setToolTip("Impress Voice Input - 语音输入就绪");
updateTrayIcon("语音输入就绪");
trayIcon_->show(); trayIcon_->show();
// 双击托盘显示窗口 // 双击托盘显示窗口
@ -146,51 +153,33 @@ void MainWindow::setupTrayIcon() {
} }
}); });
// 发送通知气泡,让用户注意到托盘图标 LOG_INFO(kTag, "系统托盘图标已创建");
trayIcon_->showMessage(
"Impress Voice Input",
"语音输入已就绪CapsLock 快捷键已注册",
QSystemTrayIcon::Information,
3000
);
LOG_INFO(kTag, QString("系统托盘图标已创建 (可用: %1, 已设置 quitOnLastWindowClosed=false)")
.arg(QSystemTrayIcon::isSystemTrayAvailable()));
} }
void MainWindow::updateTrayIcon(const QString& status) { void MainWindow::updateTrayIcon(const QString& status) {
if (!trayIcon_) return; if (!trayIcon_ || trayIcons_.isEmpty()) return;
QColor color; // 状态映射到缓存图标
const QIcon* icon = nullptr;
// 根据状态文字匹配图标颜色
if (status.contains("正在录音")) { if (status.contains("正在录音")) {
color = QColor("#e74c3c"); // 红色 - 录音中 icon = &trayIcons_["recording"];
} else if (status.contains("正在识别")) { } else if (status.contains("正在识别")) {
color = QColor("#f39c12"); // 橙色 - 识别中 icon = &trayIcons_["recognizing"];
} else if (status.contains("等待长按") || status.contains("PreRecording")) { } else if (status.contains("等待长按") || status.contains("PreRecording")) {
color = QColor("#f1c40f"); // 黄色 - 预录音 icon = &trayIcons_["waiting"];
} else if (status.contains("已启动") || status.contains("就绪")) { } else if (status.contains("已启动") || status.contains("就绪")) {
color = QColor("#27ae60"); // 绿色 - 就绪 icon = &trayIcons_["ready"];
} else if (status.contains("已关闭") || status.contains("停止")) { } else if (status.contains("已关闭") || status.contains("停止")) {
color = QColor("#95a5a6"); // 灰色 - 停止 icon = &trayIcons_["stopped"];
} else { } else {
color = QColor("#3498db"); // 蓝色 - 其他 icon = &trayIcons_["other"];
} }
QPixmap pm = createTrayIcon(color); trayIcon_->setIcon(*icon);
QIcon icon(pm);
if (icon.isNull()) {
LOG_ERROR(kTag, "托盘图标创建失败");
return;
}
trayIcon_->setIcon(icon);
trayIcon_->setToolTip(QString("Impress Voice Input - %1").arg(status)); trayIcon_->setToolTip(QString("Impress Voice Input - %1").arg(status));
} }
QPixmap MainWindow::createTrayIcon(const QColor& color) { QPixmap MainWindow::createTrayIcon(const QColor& color) {
// 使用 16x16 标准托盘图标尺寸
const int size = 16; const int size = 16;
QImage image(size, size, QImage::Format_ARGB32); QImage image(size, size, QImage::Format_ARGB32);
image.fill(Qt::transparent); image.fill(Qt::transparent);
@ -198,7 +187,6 @@ QPixmap MainWindow::createTrayIcon(const QColor& color) {
QPainter painter(&image); QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::Antialiasing);
// 绘制实心彩色圆
painter.setBrush(color); painter.setBrush(color);
painter.setPen(Qt::NoPen); painter.setPen(Qt::NoPen);
int margin = 1; int margin = 1;
@ -219,13 +207,7 @@ void MainWindow::setupMenuBar() {
auto* exitAction = fileMenu->addAction("退出"); auto* exitAction = fileMenu->addAction("退出");
exitAction->setShortcut(QKeySequence("Ctrl+Q")); exitAction->setShortcut(QKeySequence("Ctrl+Q"));
connect(exitAction, &QAction::triggered, this, [this]() { connect(exitAction, &QAction::triggered, this, [this]() {
if (trayIcon_) { doExit();
trayIcon_->hide();
}
if (voiceInputService_) {
voiceInputService_->stop();
}
qApp->quit();
}); });
// 帮助菜单 // 帮助菜单
@ -249,14 +231,20 @@ void MainWindow::loadStyleSheet() {
} }
void MainWindow::closeEvent(QCloseEvent* event) { void MainWindow::closeEvent(QCloseEvent* event) {
if (trayIcon_) { LOG_INFO(kTag, "主窗口关闭");
trayIcon_->hide(); doExit();
} QMainWindow::closeEvent(event);
}
void MainWindow::doExit() {
LOG_INFO(kTag, "应用退出");
if (voiceInputService_) { if (voiceInputService_) {
voiceInputService_->stop(); voiceInputService_->stop();
} }
LOG_INFO(kTag, "主窗口关闭 (应用保持运行,托盘图标可见)"); if (trayIcon_) {
event->ignore(); // 不关闭应用,只隐藏窗口 trayIcon_->hide();
}
qApp->quit();
} }
void MainWindow::updateModelStatus() { void MainWindow::updateModelStatus() {

View File

@ -2,11 +2,13 @@
#include <QMainWindow> #include <QMainWindow>
#include <QTabWidget> #include <QTabWidget>
#include <QMap>
#include <memory> #include <memory>
class QLabel; class QLabel;
class QSystemTrayIcon; class QSystemTrayIcon;
class QMenu; class QMenu;
class QIcon;
namespace impress { namespace impress {
@ -44,6 +46,7 @@ private:
void loadStyleSheet(); void loadStyleSheet();
void onVoiceInputConfigChanged(); void onVoiceInputConfigChanged();
void updateModelStatus(); void updateModelStatus();
void doExit();
ConfigManager* configManager_; ConfigManager* configManager_;
SenseVoiceEngine* sttEngine_; SenseVoiceEngine* sttEngine_;
@ -55,6 +58,7 @@ private:
QLabel* modelStatusLabel_; QLabel* modelStatusLabel_;
QSystemTrayIcon* trayIcon_ = nullptr; QSystemTrayIcon* trayIcon_ = nullptr;
QMenu* trayMenu_ = nullptr; QMenu* trayMenu_ = nullptr;
QMap<QString, QIcon> trayIcons_;
}; };
} // namespace impress } // namespace impress