- 新增 C API shim (ort_api_shim.h) 解决 MinGW 与 ONNX Runtime 的 SAL 注解/_stdcall 兼容性问题 - 新增轻量级 C++ 包装器 (ort_minimal) 替代 onnxruntime_cxx_api.h - cmake/dependencies.cmake 支持 Windows/ Linux 平台自动识别依赖路径 - 修复音频采集 paNonInterleaved bug(指针被误解析为 float 导致 RMS=inf) - 修复 Windows 热键和 UI 相关代码 - 添加 MinGW 交叉编译工具链配置 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
227 lines
6.9 KiB
C++
227 lines
6.9 KiB
C++
#include "win_hotkey.h"
|
||
#include "utils/logger.h"
|
||
|
||
#ifdef Q_OS_WIN
|
||
#include <windows.h>
|
||
#include <QAbstractNativeEventFilter>
|
||
#include <QGuiApplication>
|
||
#include <QThread>
|
||
#include <QWidget>
|
||
#endif
|
||
|
||
static const char* const kTag = "CapsLockVoiceHotkey";
|
||
|
||
namespace impress {
|
||
|
||
struct CapsLockVoiceHotkey::Impl {
|
||
#ifdef Q_OS_WIN
|
||
int hotkeyId = 0;
|
||
QTimer* longPressTimer = nullptr;
|
||
QTimer* keyUpDebounce = nullptr;
|
||
bool isHolding = false;
|
||
bool longPressFired = false;
|
||
bool pollThreadRunning = false;
|
||
void* nativeEventFilter = nullptr;
|
||
QWidget* hiddenWindow = nullptr;
|
||
static constexpr int kLongPressMs = 1000;
|
||
#endif
|
||
};
|
||
|
||
#ifdef Q_OS_WIN
|
||
/** Native event filter to catch WM_HOTKEY */
|
||
class HotkeyNativeEventFilter : public QAbstractNativeEventFilter {
|
||
public:
|
||
explicit HotkeyNativeEventFilter(CapsLockVoiceHotkey* hotkey)
|
||
: hotkey_(hotkey) {}
|
||
|
||
bool nativeEventFilter(const QByteArray& eventType, void* message,
|
||
qintptr* /*result*/) override {
|
||
if (eventType == "windows_generic_MSG" || eventType == "windows_dispatcher_MSG") {
|
||
auto* msg = static_cast<MSG*>(message);
|
||
if (msg->message == WM_HOTKEY) {
|
||
if (hotkey_) {
|
||
hotkey_->onHotkeyEvent(static_cast<int>(msg->wParam));
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private:
|
||
CapsLockVoiceHotkey* hotkey_ = nullptr;
|
||
};
|
||
#endif
|
||
|
||
CapsLockVoiceHotkey::CapsLockVoiceHotkey(QObject* parent)
|
||
: QObject(parent)
|
||
, impl_(std::make_unique<Impl>())
|
||
{}
|
||
|
||
CapsLockVoiceHotkey::~CapsLockVoiceHotkey() {
|
||
stop();
|
||
}
|
||
|
||
bool CapsLockVoiceHotkey::start() {
|
||
if (active_) return true;
|
||
|
||
#ifdef Q_OS_WIN
|
||
// 创建隐藏窗口用于接收 WM_HOTKEY 消息
|
||
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;
|
||
emit ready();
|
||
LOG_INFO(kTag, "CapsLock 快捷键已注册");
|
||
return true;
|
||
#else
|
||
emit error("CapsLockVoiceHotkey 仅支持 Windows 平台");
|
||
return false;
|
||
#endif
|
||
}
|
||
|
||
void CapsLockVoiceHotkey::stop() {
|
||
if (!active_) return;
|
||
|
||
#ifdef Q_OS_WIN
|
||
impl_->pollThreadRunning = false;
|
||
|
||
if (impl_->longPressTimer) {
|
||
impl_->longPressTimer->stop();
|
||
}
|
||
if (impl_->keyUpDebounce) {
|
||
impl_->keyUpDebounce->stop();
|
||
}
|
||
|
||
// 移除原生事件过滤器
|
||
if (impl_->nativeEventFilter) {
|
||
auto* filter = static_cast<HotkeyNativeEventFilter*>(impl_->nativeEventFilter);
|
||
QGuiApplication::instance()->removeNativeEventFilter(filter);
|
||
delete filter;
|
||
impl_->nativeEventFilter = nullptr;
|
||
}
|
||
|
||
// 注销快捷键
|
||
if (impl_->hiddenWindow) {
|
||
HWND hwnd = reinterpret_cast<HWND>(impl_->hiddenWindow->winId());
|
||
if (hwnd && impl_->hotkeyId) {
|
||
UnregisterHotKey(hwnd, impl_->hotkeyId);
|
||
GlobalDeleteAtom(impl_->hotkeyId);
|
||
impl_->hotkeyId = 0;
|
||
}
|
||
impl_->hiddenWindow->hide();
|
||
}
|
||
|
||
active_ = false;
|
||
recording_ = false;
|
||
impl_->isHolding = false;
|
||
impl_->longPressFired = false;
|
||
LOG_INFO(kTag, "CapsLock 快捷键已停止");
|
||
#endif
|
||
}
|
||
|
||
#ifdef Q_OS_WIN
|
||
void CapsLockVoiceHotkey::onHotkeyEvent(int /*hotkeyId*/) {
|
||
if (!active_) return;
|
||
|
||
// Windows 只在按键按下时触发 WM_HOTKEY
|
||
// 我们通过 GetAsyncKeyState 轮询检测松开
|
||
impl_->isHolding = true;
|
||
impl_->longPressFired = false;
|
||
|
||
// 启动长按定时器
|
||
if (impl_->longPressTimer) {
|
||
impl_->longPressTimer->start();
|
||
}
|
||
|
||
// 启动轮询线程检测松开
|
||
if (impl_->pollThreadRunning) return;
|
||
impl_->pollThreadRunning = true;
|
||
QThread::create([this]() {
|
||
const int vkCapsLock = 0x14;
|
||
while (impl_->pollThreadRunning && impl_->isHolding) {
|
||
SHORT state = GetAsyncKeyState(vkCapsLock);
|
||
if (!(state & 0x8000)) {
|
||
// 按键松开
|
||
impl_->isHolding = false;
|
||
if (impl_->longPressTimer) {
|
||
impl_->longPressTimer->stop();
|
||
}
|
||
QMetaObject::invokeMethod(this, [this]() {
|
||
if (impl_->longPressFired) {
|
||
// 长按 → 停止录音
|
||
recording_ = false;
|
||
emit recordingStopped();
|
||
LOG_DEBUG(kTag, "长按结束,停止录音");
|
||
} else {
|
||
// 短按 → 不处理(让系统处理 CapsLock)
|
||
LOG_DEBUG(kTag, "短按,不拦截 CapsLock");
|
||
}
|
||
impl_->longPressFired = false;
|
||
if (impl_->keyUpDebounce) {
|
||
impl_->keyUpDebounce->start();
|
||
}
|
||
}, Qt::QueuedConnection);
|
||
break;
|
||
}
|
||
QThread::msleep(50);
|
||
}
|
||
impl_->pollThreadRunning = false;
|
||
})->start();
|
||
}
|
||
#endif
|
||
|
||
} // namespace impress
|