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:
parent
6bf22041f8
commit
8425fb4a04
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user