fix(windows): 修复热键单键支持和隐藏窗口闪烁
- setHotkeyCombo 支持无修饰键的单键(CapsLock/F1 等) - 新增 CapsLock 及特殊按键到 Win32 VK 映射 - 热键窗口改为纯 Qt 方式隐藏,不再闪烁(winId+move 屏幕外) - 解析失败时 emit error 信号并记录日志,不再静默忽略 - 添加详细调试日志用于问题定位 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
20bc24a028
commit
2c8b2a8ad9
@ -7,6 +7,7 @@
|
|||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
#include <QTimer>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static const char* const kTag = "CapsLockVoiceHotkey";
|
static const char* const kTag = "CapsLockVoiceHotkey";
|
||||||
@ -16,14 +17,11 @@ namespace impress {
|
|||||||
struct CapsLockVoiceHotkey::Impl {
|
struct CapsLockVoiceHotkey::Impl {
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
int hotkeyId = 0;
|
int hotkeyId = 0;
|
||||||
QTimer* longPressTimer = nullptr;
|
QTimer* keyUpTimer = nullptr; // 检测松开的轮询定时器
|
||||||
QTimer* keyUpDebounce = nullptr;
|
|
||||||
bool isHolding = false;
|
|
||||||
bool longPressFired = false;
|
|
||||||
bool pollThreadRunning = false;
|
|
||||||
void* nativeEventFilter = nullptr;
|
|
||||||
QWidget* hiddenWindow = nullptr;
|
QWidget* hiddenWindow = nullptr;
|
||||||
static constexpr int kLongPressMs = 1000;
|
void* nativeEventFilter = nullptr;
|
||||||
|
bool isHolding = false;
|
||||||
|
bool pollThreadRunning = false;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,78 +51,132 @@ private:
|
|||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/** Qt::KeyboardModifiers → Win32 MOD_* */
|
||||||
|
static int qtModsToWin32(int qtMods) {
|
||||||
|
int mods = 0;
|
||||||
|
if (qtMods & Qt::ControlModifier) mods |= MOD_CONTROL;
|
||||||
|
if (qtMods & Qt::AltModifier) mods |= MOD_ALT;
|
||||||
|
if (qtMods & Qt::ShiftModifier) mods |= MOD_SHIFT;
|
||||||
|
if (qtMods & Qt::MetaModifier) mods |= MOD_WIN;
|
||||||
|
return mods;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Qt::Key → Win32 virtual key code */
|
||||||
|
static int qtKeyToWin32(int key) {
|
||||||
|
if (key >= Qt::Key_A && key <= Qt::Key_Z) {
|
||||||
|
return 'A' + (key - Qt::Key_A);
|
||||||
|
}
|
||||||
|
if (key >= Qt::Key_0 && key <= Qt::Key_9) {
|
||||||
|
return '0' + (key - Qt::Key_0);
|
||||||
|
}
|
||||||
|
if (key >= Qt::Key_F1 && key <= Qt::Key_F12) {
|
||||||
|
return VK_F1 + (key - Qt::Key_F1);
|
||||||
|
}
|
||||||
|
switch (key) {
|
||||||
|
case Qt::Key_Space: return VK_SPACE;
|
||||||
|
case Qt::Key_Return:
|
||||||
|
case Qt::Key_Enter: return VK_RETURN;
|
||||||
|
case Qt::Key_Escape: return VK_ESCAPE;
|
||||||
|
case Qt::Key_Tab: return VK_TAB;
|
||||||
|
case Qt::Key_Backspace: return VK_BACK;
|
||||||
|
case Qt::Key_Delete: return VK_DELETE;
|
||||||
|
case Qt::Key_Insert: return VK_INSERT;
|
||||||
|
case Qt::Key_Home: return VK_HOME;
|
||||||
|
case Qt::Key_End: return VK_END;
|
||||||
|
case Qt::Key_PageUp: return VK_PRIOR;
|
||||||
|
case Qt::Key_PageDown: return VK_NEXT;
|
||||||
|
case Qt::Key_Up: return VK_UP;
|
||||||
|
case Qt::Key_Down: return VK_DOWN;
|
||||||
|
case Qt::Key_Left: return VK_LEFT;
|
||||||
|
case Qt::Key_Right: return VK_RIGHT;
|
||||||
|
case Qt::Key_CapsLock: return VK_CAPITAL;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CapsLockVoiceHotkey::CapsLockVoiceHotkey(QObject* parent)
|
CapsLockVoiceHotkey::CapsLockVoiceHotkey(QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, impl_(std::make_unique<Impl>())
|
, impl_(std::make_unique<Impl>())
|
||||||
{}
|
{
|
||||||
|
hotkeyCombo_ = "Ctrl+Alt+C";
|
||||||
|
}
|
||||||
|
|
||||||
CapsLockVoiceHotkey::~CapsLockVoiceHotkey() {
|
CapsLockVoiceHotkey::~CapsLockVoiceHotkey() {
|
||||||
stop();
|
stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CapsLockVoiceHotkey::setHotkeyCombo(const QString& combo) {
|
||||||
|
hotkeyCombo_ = combo;
|
||||||
|
|
||||||
|
int mods = 0, key = 0;
|
||||||
|
QStringList parts = combo.split('+', Qt::SkipEmptyParts);
|
||||||
|
|
||||||
|
// 解析修饰键(除了最后一个部分)
|
||||||
|
for (int i = 0; i < parts.size() - 1; i++) {
|
||||||
|
QString part = parts[i].trimmed().toLower();
|
||||||
|
if (part == "ctrl" || part == "control") mods |= Qt::ControlModifier;
|
||||||
|
else if (part == "alt") mods |= Qt::AltModifier;
|
||||||
|
else if (part == "shift") mods |= Qt::ShiftModifier;
|
||||||
|
else if (part == "meta" || part == "super" || part == "win") mods |= Qt::MetaModifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析主键(最后一个部分)
|
||||||
|
QString mainKey = parts.last().trimmed();
|
||||||
|
if (mainKey.length() == 1 && mainKey[0].isLetter()) {
|
||||||
|
key = Qt::Key_A + (mainKey[0].toUpper().unicode() - 'A');
|
||||||
|
} else if (mainKey.length() == 1 && mainKey[0].isDigit()) {
|
||||||
|
key = Qt::Key_0 + (mainKey[0].unicode() - '0');
|
||||||
|
} else {
|
||||||
|
// 特殊按键映射
|
||||||
|
QString mk = mainKey.toLower();
|
||||||
|
if (mk == "capslock") key = Qt::Key_CapsLock;
|
||||||
|
else if (mk == "space") key = Qt::Key_Space;
|
||||||
|
else if (mk == "enter" || mk == "return") key = Qt::Key_Return;
|
||||||
|
else if (mk == "esc") key = Qt::Key_Escape;
|
||||||
|
else if (mk == "tab") key = Qt::Key_Tab;
|
||||||
|
else if (mk == "backspace") key = Qt::Key_Backspace;
|
||||||
|
else if (mk == "delete") key = Qt::Key_Delete;
|
||||||
|
else if (mk == "insert") key = Qt::Key_Insert;
|
||||||
|
else if (mk == "home") key = Qt::Key_Home;
|
||||||
|
else if (mk == "end") key = Qt::Key_End;
|
||||||
|
else if (mk == "pageup") key = Qt::Key_PageUp;
|
||||||
|
else if (mk == "pagedown") key = Qt::Key_PageDown;
|
||||||
|
else if (mk == "up") key = Qt::Key_Up;
|
||||||
|
else if (mk == "down") key = Qt::Key_Down;
|
||||||
|
else if (mk == "left") key = Qt::Key_Left;
|
||||||
|
else if (mk == "right") key = Qt::Key_Right;
|
||||||
|
else if (mk.startsWith('f') && mk.length() >= 2) {
|
||||||
|
int num = mk.mid(1).toInt();
|
||||||
|
if (num >= 1 && num <= 12) key = Qt::Key_F1 + (num - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == 0) {
|
||||||
|
LOG_ERROR(kTag, QString("无法解析快捷键组合: %1").arg(combo));
|
||||||
|
emit error(QString("无效的快捷键: %1").arg(combo));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifiers_ = qtModsToWin32(mods);
|
||||||
|
vkKey_ = qtKeyToWin32(key);
|
||||||
|
|
||||||
|
LOG_INFO(kTag, QString("快捷键已更新: %1 (mods=%2, vk=0x%3)").arg(combo).arg(modifiers_).arg(vkKey_, 0, 16));
|
||||||
|
|
||||||
|
if (active_) {
|
||||||
|
unregisterHotkey();
|
||||||
|
registerHotkey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool CapsLockVoiceHotkey::start() {
|
bool CapsLockVoiceHotkey::start() {
|
||||||
if (active_) return true;
|
if (active_) return true;
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
// 创建隐藏窗口用于接收 WM_HOTKEY 消息
|
registerHotkey();
|
||||||
if (!impl_->hiddenWindow) {
|
|
||||||
impl_->hiddenWindow = new QWidget();
|
|
||||||
impl_->hiddenWindow->setObjectName("HotkeyReceiver");
|
|
||||||
impl_->hiddenWindow->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint);
|
|
||||||
impl_->hiddenWindow->resize(0, 0);
|
|
||||||
}
|
|
||||||
// 确保窗口已创建(show 会创建原生句柄)
|
|
||||||
impl_->hiddenWindow->show();
|
|
||||||
|
|
||||||
HWND hwnd = reinterpret_cast<HWND>(impl_->hiddenWindow->winId());
|
|
||||||
if (!hwnd) {
|
|
||||||
emit error("无法创建窗口句柄");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册 CapsLock (VK_CAPITAL = 0x14) 全局快捷键
|
|
||||||
// MOD_NOREPEAT 防止按住时重复触发
|
|
||||||
const int vkCapsLock = 0x14;
|
|
||||||
impl_->hotkeyId = GlobalAddAtom(L"ImpressVoiceHotkey");
|
|
||||||
|
|
||||||
BOOL ok = RegisterHotKey(hwnd, impl_->hotkeyId, MOD_NOREPEAT, vkCapsLock);
|
|
||||||
if (!ok) {
|
|
||||||
DWORD err = GetLastError();
|
|
||||||
emit error(QString("注册 CapsLock 快捷键失败 (错误码: %1)").arg(err));
|
|
||||||
LOG_ERROR(kTag, QString("RegisterHotKey failed: %1").arg(err));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 安装原生事件过滤器
|
|
||||||
auto* filter = new HotkeyNativeEventFilter(this);
|
|
||||||
QGuiApplication::instance()->installNativeEventFilter(filter);
|
|
||||||
impl_->nativeEventFilter = filter;
|
|
||||||
|
|
||||||
// 长按定时器
|
|
||||||
impl_->longPressTimer = new QTimer(this);
|
|
||||||
impl_->longPressTimer->setSingleShot(true);
|
|
||||||
impl_->longPressTimer->setInterval(Impl::kLongPressMs);
|
|
||||||
connect(impl_->longPressTimer, &QTimer::timeout, this, [this]() {
|
|
||||||
if (impl_->isHolding && !impl_->longPressFired) {
|
|
||||||
impl_->longPressFired = true;
|
|
||||||
recording_ = true;
|
|
||||||
emit recordingStarted();
|
|
||||||
LOG_DEBUG(kTag, "长按触发,开始录音");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 松开后延迟重置,避免 CapsLock 状态闪烁
|
|
||||||
impl_->keyUpDebounce = new QTimer(this);
|
|
||||||
impl_->keyUpDebounce->setSingleShot(true);
|
|
||||||
impl_->keyUpDebounce->setInterval(200);
|
|
||||||
connect(impl_->keyUpDebounce, &QTimer::timeout, this, [this]() {
|
|
||||||
impl_->isHolding = false;
|
|
||||||
impl_->longPressFired = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
active_ = true;
|
active_ = true;
|
||||||
emit ready();
|
emit ready();
|
||||||
LOG_INFO(kTag, "CapsLock 快捷键已注册");
|
LOG_INFO(kTag, QString("快捷键已注册(%1)").arg(hotkeyCombo_));
|
||||||
return true;
|
return true;
|
||||||
#else
|
#else
|
||||||
emit error("CapsLockVoiceHotkey 仅支持 Windows 平台");
|
emit error("CapsLockVoiceHotkey 仅支持 Windows 平台");
|
||||||
@ -137,15 +189,61 @@ void CapsLockVoiceHotkey::stop() {
|
|||||||
|
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
impl_->pollThreadRunning = false;
|
impl_->pollThreadRunning = false;
|
||||||
|
unregisterHotkey();
|
||||||
|
|
||||||
if (impl_->longPressTimer) {
|
active_ = false;
|
||||||
impl_->longPressTimer->stop();
|
recording_ = false;
|
||||||
}
|
impl_->isHolding = false;
|
||||||
if (impl_->keyUpDebounce) {
|
LOG_INFO(kTag, "快捷键已停止");
|
||||||
impl_->keyUpDebounce->stop();
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
void CapsLockVoiceHotkey::registerHotkey() {
|
||||||
|
if (!impl_->hiddenWindow) {
|
||||||
|
impl_->hiddenWindow = new QWidget();
|
||||||
|
impl_->hiddenWindow->setObjectName("HotkeyReceiver");
|
||||||
|
impl_->hiddenWindow->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint);
|
||||||
|
impl_->hiddenWindow->resize(1, 1);
|
||||||
|
impl_->hiddenWindow->move(-10000, -10000); // 移到屏幕外
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过 winId() 创建原生窗口(不显示)
|
||||||
|
WId wid = impl_->hiddenWindow->winId();
|
||||||
|
HWND hwnd = reinterpret_cast<HWND>(wid);
|
||||||
|
if (!hwnd) {
|
||||||
|
emit error("无法创建窗口句柄");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_->hotkeyId = GlobalAddAtom(L"ImpressVoiceHotkey");
|
||||||
|
|
||||||
|
// MOD_NOREPEAT 防止按住时重复触发 WM_HOTKEY
|
||||||
|
BOOL ok = RegisterHotKey(hwnd, impl_->hotkeyId, modifiers_ | MOD_NOREPEAT, vkKey_);
|
||||||
|
if (!ok) {
|
||||||
|
DWORD err = GetLastError();
|
||||||
|
emit error(QString("注册快捷键 %1 失败 (错误码: %2)").arg(hotkeyCombo_).arg(err));
|
||||||
|
LOG_ERROR(kTag, QString("RegisterHotKey failed: %1").arg(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(kTag, QString("热键窗口已注册 (HWND=%1)").arg((qulonglong)hwnd));
|
||||||
|
|
||||||
|
auto* filter = new HotkeyNativeEventFilter(this);
|
||||||
|
QGuiApplication::instance()->installNativeEventFilter(filter);
|
||||||
|
impl_->nativeEventFilter = filter;
|
||||||
|
|
||||||
|
// keyUpTimer 用于检测松开
|
||||||
|
impl_->keyUpTimer = new QTimer(this);
|
||||||
|
impl_->keyUpTimer->setSingleShot(true);
|
||||||
|
impl_->keyUpTimer->setInterval(500); // 最长等待 500ms
|
||||||
|
}
|
||||||
|
|
||||||
|
void CapsLockVoiceHotkey::unregisterHotkey() {
|
||||||
|
if (impl_->keyUpTimer) {
|
||||||
|
impl_->keyUpTimer->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除原生事件过滤器
|
|
||||||
if (impl_->nativeEventFilter) {
|
if (impl_->nativeEventFilter) {
|
||||||
auto* filter = static_cast<HotkeyNativeEventFilter*>(impl_->nativeEventFilter);
|
auto* filter = static_cast<HotkeyNativeEventFilter*>(impl_->nativeEventFilter);
|
||||||
QGuiApplication::instance()->removeNativeEventFilter(filter);
|
QGuiApplication::instance()->removeNativeEventFilter(filter);
|
||||||
@ -153,9 +251,9 @@ void CapsLockVoiceHotkey::stop() {
|
|||||||
impl_->nativeEventFilter = nullptr;
|
impl_->nativeEventFilter = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注销快捷键
|
|
||||||
if (impl_->hiddenWindow) {
|
if (impl_->hiddenWindow) {
|
||||||
HWND hwnd = reinterpret_cast<HWND>(impl_->hiddenWindow->winId());
|
WId wid = impl_->hiddenWindow->winId();
|
||||||
|
HWND hwnd = reinterpret_cast<HWND>(wid);
|
||||||
if (hwnd && impl_->hotkeyId) {
|
if (hwnd && impl_->hotkeyId) {
|
||||||
UnregisterHotKey(hwnd, impl_->hotkeyId);
|
UnregisterHotKey(hwnd, impl_->hotkeyId);
|
||||||
GlobalDeleteAtom(impl_->hotkeyId);
|
GlobalDeleteAtom(impl_->hotkeyId);
|
||||||
@ -163,60 +261,72 @@ void CapsLockVoiceHotkey::stop() {
|
|||||||
}
|
}
|
||||||
impl_->hiddenWindow->hide();
|
impl_->hiddenWindow->hide();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
active_ = false;
|
|
||||||
recording_ = false;
|
|
||||||
impl_->isHolding = false;
|
|
||||||
impl_->longPressFired = false;
|
|
||||||
LOG_INFO(kTag, "CapsLock 快捷键已停止");
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void CapsLockVoiceHotkey::toggleRecording() {
|
||||||
|
if (!active_ || ignoreEvents_) return;
|
||||||
|
|
||||||
|
if (recording_) {
|
||||||
|
recording_ = false;
|
||||||
|
emit recordingStopped();
|
||||||
|
} else {
|
||||||
|
recording_ = true;
|
||||||
|
emit recordingStarted();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
void CapsLockVoiceHotkey::onHotkeyEvent(int /*hotkeyId*/) {
|
void CapsLockVoiceHotkey::onHotkeyEvent(int /*hotkeyId*/) {
|
||||||
if (!active_) return;
|
if (!active_ || impl_->isHolding) return;
|
||||||
|
|
||||||
// Windows 只在按键按下时触发 WM_HOTKEY
|
LOG_DEBUG(kTag, "快捷键按下,开始录音");
|
||||||
// 我们通过 GetAsyncKeyState 轮询检测松开
|
recording_ = true;
|
||||||
impl_->isHolding = true;
|
impl_->isHolding = true;
|
||||||
impl_->longPressFired = false;
|
emit recordingStarted();
|
||||||
|
|
||||||
// 启动长按定时器
|
// 启动轮询线程检测松开(任何修饰键或主键松开即停止)
|
||||||
if (impl_->longPressTimer) {
|
|
||||||
impl_->longPressTimer->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动轮询线程检测松开
|
|
||||||
if (impl_->pollThreadRunning) return;
|
if (impl_->pollThreadRunning) return;
|
||||||
impl_->pollThreadRunning = true;
|
impl_->pollThreadRunning = true;
|
||||||
QThread::create([this]() {
|
|
||||||
const int vkCapsLock = 0x14;
|
const int vk = vkKey_;
|
||||||
|
const int mods = modifiers_;
|
||||||
|
|
||||||
|
QThread::create([this, vk, mods]() {
|
||||||
|
// 等待所有相关键松开
|
||||||
while (impl_->pollThreadRunning && impl_->isHolding) {
|
while (impl_->pollThreadRunning && impl_->isHolding) {
|
||||||
SHORT state = GetAsyncKeyState(vkCapsLock);
|
// 检查主键
|
||||||
if (!(state & 0x8000)) {
|
SHORT mainState = GetAsyncKeyState(vk);
|
||||||
// 按键松开
|
bool mainHeld = (mainState & 0x8000) != 0;
|
||||||
|
|
||||||
|
// 检查修饰键
|
||||||
|
bool ctrlHeld = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||||
|
bool altHeld = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0;
|
||||||
|
bool shiftHeld = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
|
||||||
|
bool winHeld = (GetAsyncKeyState(VK_LWIN) & 0x8000) != 0 ||
|
||||||
|
(GetAsyncKeyState(VK_RWIN) & 0x8000) != 0;
|
||||||
|
|
||||||
|
bool modsHeld = true;
|
||||||
|
if (mods & MOD_CONTROL && !ctrlHeld) modsHeld = false;
|
||||||
|
if (mods & MOD_ALT && !altHeld) modsHeld = false;
|
||||||
|
if (mods & MOD_SHIFT && !shiftHeld) modsHeld = false;
|
||||||
|
if (mods & MOD_WIN && !winHeld) modsHeld = false;
|
||||||
|
|
||||||
|
if (!mainHeld || !modsHeld) {
|
||||||
|
// 任一组合键松开
|
||||||
impl_->isHolding = false;
|
impl_->isHolding = false;
|
||||||
if (impl_->longPressTimer) {
|
|
||||||
impl_->longPressTimer->stop();
|
|
||||||
}
|
|
||||||
QMetaObject::invokeMethod(this, [this]() {
|
QMetaObject::invokeMethod(this, [this]() {
|
||||||
if (impl_->longPressFired) {
|
if (recording_) {
|
||||||
// 长按 → 停止录音
|
LOG_DEBUG(kTag, "快捷键松开,停止录音");
|
||||||
recording_ = false;
|
recording_ = false;
|
||||||
emit recordingStopped();
|
emit recordingStopped();
|
||||||
LOG_DEBUG(kTag, "长按结束,停止录音");
|
|
||||||
} else {
|
|
||||||
// 短按 → 不处理(让系统处理 CapsLock)
|
|
||||||
LOG_DEBUG(kTag, "短按,不拦截 CapsLock");
|
|
||||||
}
|
|
||||||
impl_->longPressFired = false;
|
|
||||||
if (impl_->keyUpDebounce) {
|
|
||||||
impl_->keyUpDebounce->start();
|
|
||||||
}
|
}
|
||||||
|
impl_->isHolding = false;
|
||||||
}, Qt::QueuedConnection);
|
}, Qt::QueuedConnection);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
QThread::msleep(50);
|
QThread::msleep(30);
|
||||||
}
|
}
|
||||||
impl_->pollThreadRunning = false;
|
impl_->pollThreadRunning = false;
|
||||||
})->start();
|
})->start();
|
||||||
|
|||||||
@ -2,20 +2,15 @@
|
|||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QTimer>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace impress {
|
namespace impress {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief CapsLock 长按语音输入快捷键管理器(Windows)
|
* @brief 全局语音输入快捷键管理器(Windows)
|
||||||
*
|
*
|
||||||
* 使用 Windows RegisterHotKey API 实现全局快捷键。
|
* 使用 Windows RegisterHotKey API 实现全局快捷键。
|
||||||
* 工作流程:
|
* 切换模式:按一次开始录音,再按一次停止录音并转写。
|
||||||
* 1. 用户长按 CapsLock 1 秒后触发录音
|
|
||||||
* 2. 长按期间持续录音
|
|
||||||
* 3. 松开 CapsLock 后停止录音并触发转写
|
|
||||||
* 4. 短按(< 1s)直接传递 CapsLock 事件(切换大小写锁定)
|
|
||||||
*/
|
*/
|
||||||
class CapsLockVoiceHotkey : public QObject {
|
class CapsLockVoiceHotkey : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@ -32,20 +27,19 @@ public:
|
|||||||
/** @brief 是否已激活 */
|
/** @brief 是否已激活 */
|
||||||
bool isActive() const { return active_; }
|
bool isActive() const { return active_; }
|
||||||
|
|
||||||
/** @brief 当前是否正在录音(CapsLock 长按超过 1s 后) */
|
/** @brief 当前是否正在录音 */
|
||||||
bool isRecording() const { return recording_; }
|
bool isRecording() const { return recording_; }
|
||||||
|
|
||||||
|
/** @brief 临时忽略信号 */
|
||||||
|
void setIgnoreEvents(bool ignore) { ignoreEvents_ = ignore; }
|
||||||
|
|
||||||
|
/** @brief 设置快捷键组合(如 "Ctrl+Alt+C") */
|
||||||
|
void setHotkeyCombo(const QString& combo);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
/** @brief 开始录音(长按超过 1 秒后) */
|
|
||||||
void recordingStarted();
|
void recordingStarted();
|
||||||
|
|
||||||
/** @brief 停止录音(松开快捷键后) */
|
|
||||||
void recordingStopped();
|
void recordingStopped();
|
||||||
|
|
||||||
/** @brief 快捷键已注册 */
|
|
||||||
void ready();
|
void ready();
|
||||||
|
|
||||||
/** @brief 初始化失败 */
|
|
||||||
void error(const QString& message);
|
void error(const QString& message);
|
||||||
|
|
||||||
/** @brief 处理 WM_HOTKEY 事件(由原生事件过滤器调用) */
|
/** @brief 处理 WM_HOTKEY 事件(由原生事件过滤器调用) */
|
||||||
@ -57,6 +51,14 @@ 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;
|
||||||
|
QString hotkeyCombo_;
|
||||||
|
int modifiers_ = 0x0002 | 0x0001; // MOD_CONTROL | MOD_ALT
|
||||||
|
int vkKey_ = 'C';
|
||||||
|
|
||||||
|
void registerHotkey();
|
||||||
|
void unregisterHotkey();
|
||||||
|
void toggleRecording();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace impress
|
} // namespace impress
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user