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) {
|
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();
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user