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) {
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();

View File

@ -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> impl_;
bool active_ = false;
bool recording_ = false;
bool ignoreEvents_ = false;
void handleSessionResponse(uint response, const QVariantMap& results);
void handleBindResponse(uint response, const QVariantMap& results);

View File

@ -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

View File

@ -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() {

View File

@ -2,11 +2,13 @@
#include <QMainWindow>
#include <QTabWidget>
#include <QMap>
#include <memory>
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<QString, QIcon> trayIcons_;
};
} // namespace impress