添加跨平台全局热键支持PTT按住说话功能
This commit is contained in:
+11
-1
@@ -6,7 +6,7 @@ set(CMAKE_AUTOMOC ON)
|
|||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
set(CMAKE_AUTOUIC ON)
|
set(CMAKE_AUTOUIC ON)
|
||||||
|
|
||||||
set(CMAKE_PREFIX_PATH "/home/misaki/Qt6.3/6.6.3/gcc_64") # 设置Qt6安装路径(此处请根据你的Qt6安装位置填写)
|
set(CMAKE_PREFIX_PATH "/home/misaki/Qt/6.6.3/gcc_64") # 设置Qt6安装路径(此处请根据你的Qt6安装位置填写)
|
||||||
|
|
||||||
# 设置是否为debug模式 默认为Debug
|
# 设置是否为debug模式 默认为Debug
|
||||||
if(NOT CMAKE_BUILD_TYPE)
|
if(NOT CMAKE_BUILD_TYPE)
|
||||||
@@ -183,6 +183,16 @@ target_link_libraries(${PROJECT_NAME}
|
|||||||
Qt::Concurrent
|
Qt::Concurrent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 全局热键 (PTT) 所需平台库
|
||||||
|
if(APPLE)
|
||||||
|
find_library(COREGRAPHICS_LIB CoreGraphics REQUIRED)
|
||||||
|
target_link_libraries(${PROJECT_NAME} PRIVATE ${COREGRAPHICS_LIB})
|
||||||
|
endif()
|
||||||
|
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
|
find_package(X11 REQUIRED)
|
||||||
|
target_link_libraries(${PROJECT_NAME} PRIVATE X11::X11)
|
||||||
|
endif()
|
||||||
|
|
||||||
# 添加头文件
|
# 添加头文件
|
||||||
target_include_directories(${PROJECT_NAME}
|
target_include_directories(${PROJECT_NAME}
|
||||||
PRIVATE
|
PRIVATE
|
||||||
|
|||||||
+42
-2
@@ -12,12 +12,22 @@
|
|||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
#ifdef Q_OS_LINUX
|
||||||
|
#include <QAbstractNativeEventFilter>
|
||||||
|
#endif
|
||||||
#include "serialportmanager.h"
|
#include "serialportmanager.h"
|
||||||
|
|
||||||
class DeviceTcpServer;
|
class DeviceTcpServer;
|
||||||
class DeviceWebSocketServer;
|
class DeviceWebSocketServer;
|
||||||
|
|
||||||
class AppCore final : public QObject {
|
class AppCore final : public QObject
|
||||||
|
#ifdef Q_OS_LINUX
|
||||||
|
, public QAbstractNativeEventFilter
|
||||||
|
#endif
|
||||||
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_DISABLE_COPY(AppCore)
|
Q_DISABLE_COPY(AppCore)
|
||||||
|
|
||||||
@@ -44,5 +54,35 @@ public:
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
void SingleExchange();
|
void SingleExchange();
|
||||||
void tryToInit() { return; };
|
void tryToInit() { return; }
|
||||||
|
|
||||||
|
// PTT 按住说话
|
||||||
|
void startPttRecording();
|
||||||
|
void stopPttRecording();
|
||||||
|
|
||||||
|
void setupGlobalHotkey();
|
||||||
|
void cleanupGlobalHotkey();
|
||||||
|
|
||||||
|
#if defined(Q_OS_WIN)
|
||||||
|
private:
|
||||||
|
static LRESULT CALLBACK lowLevelKeyboardHook(int nCode, WPARAM wParam, LPARAM lParam);
|
||||||
|
HHOOK m_keyboardHook = nullptr;
|
||||||
|
bool m_isPttDown = false;
|
||||||
|
UINT m_hotkeyVKey = VK_OEM_3;
|
||||||
|
|
||||||
|
#elif defined(Q_OS_LINUX)
|
||||||
|
private:
|
||||||
|
bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override;
|
||||||
|
void onDebounceStop();
|
||||||
|
bool m_isPttDown = false;
|
||||||
|
int m_hotkeyCode = 0;
|
||||||
|
class QTimer *m_pttDebounce = nullptr;
|
||||||
|
|
||||||
|
#elif defined(Q_OS_MACOS)
|
||||||
|
private:
|
||||||
|
void *m_eventTap = nullptr;
|
||||||
|
void *m_runLoopSource = nullptr;
|
||||||
|
bool m_isPttDown = false;
|
||||||
|
int m_hotkeyCode = 50;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
#include "AppCore.h"
|
#include "AppCore.h"
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
#include "AudioDataHandle.h"
|
#include "AudioDataHandle.h"
|
||||||
#include "AutoAgentHandle.h"
|
#include "AutoAgentHandle.h"
|
||||||
@@ -15,10 +17,26 @@
|
|||||||
#include "websocketmanager.h"
|
#include "websocketmanager.h"
|
||||||
#include "DeviceTcpServer.h"
|
#include "DeviceTcpServer.h"
|
||||||
#include "DeviceWebSocketServer.h"
|
#include "DeviceWebSocketServer.h"
|
||||||
|
|
||||||
|
#ifdef Q_OS_LINUX
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/keysym.h>
|
||||||
|
#include <X11/XKBlib.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
#include <CoreGraphics/CoreGraphics.h>
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
// 初始化静态成员
|
// 初始化静态成员
|
||||||
QScopedPointer<AppCore> AppCore::m_instance;
|
QScopedPointer<AppCore> AppCore::m_instance;
|
||||||
QMutex AppCore::m_mutex;
|
QMutex AppCore::m_mutex;
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
static AppCore *g_hotkeyInstance = nullptr;
|
||||||
|
#endif
|
||||||
|
|
||||||
// 单例实现 (QScopedPointer + Mutex)
|
// 单例实现 (QScopedPointer + Mutex)
|
||||||
AppCore* AppCore::getInstance()
|
AppCore* AppCore::getInstance()
|
||||||
{
|
{
|
||||||
@@ -81,10 +99,13 @@ AppCore::AppCore(QObject *parent) : QObject(parent)
|
|||||||
// 连接必要的信号
|
// 连接必要的信号
|
||||||
connect(AudioInput::getInstance(), &AudioInput::recordingFinished_Byte,
|
connect(AudioInput::getInstance(), &AudioInput::recordingFinished_Byte,
|
||||||
this, &AppCore::onRecordingFinished_Byte);
|
this, &AppCore::onRecordingFinished_Byte);
|
||||||
|
|
||||||
|
setupGlobalHotkey();
|
||||||
}
|
}
|
||||||
|
|
||||||
AppCore::~AppCore()
|
AppCore::~AppCore()
|
||||||
{
|
{
|
||||||
|
cleanupGlobalHotkey();
|
||||||
if (m_deviceTcpServer) m_deviceTcpServer->stop();
|
if (m_deviceTcpServer) m_deviceTcpServer->stop();
|
||||||
if (m_deviceWsServer) m_deviceWsServer->stop();
|
if (m_deviceWsServer) m_deviceWsServer->stop();
|
||||||
|
|
||||||
@@ -118,3 +139,209 @@ void AppCore::onRecordingFinished_Byte(const QByteArray &wavData) {
|
|||||||
NetworkDO::getInstance()->sendPacket(packet);
|
NetworkDO::getInstance()->sendPacket(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AppCore::startPttRecording() {
|
||||||
|
qDebug() << "PTT recording started";
|
||||||
|
AudioInput::getInstance()->startAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppCore::stopPttRecording() {
|
||||||
|
qDebug() << "PTT recording stopped";
|
||||||
|
AudioInput::getInstance()->stopAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== Windows =====================
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
void AppCore::setupGlobalHotkey() {
|
||||||
|
g_hotkeyInstance = this;
|
||||||
|
HMODULE hMod = GetModuleHandle(nullptr);
|
||||||
|
m_keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, lowLevelKeyboardHook, hMod, 0);
|
||||||
|
if (m_keyboardHook) {
|
||||||
|
qDebug() << "[AppCore] 全局 PTT 热键钩子已安装 (键码:" << m_hotkeyVKey << ")";
|
||||||
|
} else {
|
||||||
|
qWarning() << "[AppCore] 全局 PTT 热键钩子安装失败:" << GetLastError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppCore::cleanupGlobalHotkey() {
|
||||||
|
if (m_keyboardHook) {
|
||||||
|
UnhookWindowsHookEx(m_keyboardHook);
|
||||||
|
m_keyboardHook = nullptr;
|
||||||
|
qDebug() << "[AppCore] 全局 PTT 热键钩子已卸载";
|
||||||
|
}
|
||||||
|
g_hotkeyInstance = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CALLBACK AppCore::lowLevelKeyboardHook(int nCode, WPARAM wParam, LPARAM lParam) {
|
||||||
|
if (nCode == HC_ACTION && g_hotkeyInstance) {
|
||||||
|
KBDLLHOOKSTRUCT *pKb = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
|
||||||
|
if (pKb->vkCode == g_hotkeyInstance->m_hotkeyVKey) {
|
||||||
|
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
|
||||||
|
if (!g_hotkeyInstance->m_isPttDown) {
|
||||||
|
g_hotkeyInstance->m_isPttDown = true;
|
||||||
|
QMetaObject::invokeMethod(qApp, []() {
|
||||||
|
AppCore::getInstance()->startPttRecording();
|
||||||
|
}, Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
} else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
|
||||||
|
if (g_hotkeyInstance->m_isPttDown) {
|
||||||
|
g_hotkeyInstance->m_isPttDown = false;
|
||||||
|
QMetaObject::invokeMethod(qApp, []() {
|
||||||
|
AppCore::getInstance()->stopPttRecording();
|
||||||
|
}, Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CallNextHookEx(nullptr, nCode, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== Linux (X11) =====================
|
||||||
|
#elif defined(Q_OS_LINUX)
|
||||||
|
void AppCore::setupGlobalHotkey() {
|
||||||
|
Display *display = XOpenDisplay(nullptr);
|
||||||
|
if (!display) {
|
||||||
|
qWarning() << "[AppCore] 无法打开 X11 Display,全局热键不可用";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试开启 detectable auto-repeat(不一定持久影响其他连接,不影响 debounce 方案)
|
||||||
|
int orig = 0;
|
||||||
|
XkbSetDetectableAutoRepeat(display, True, &orig);
|
||||||
|
|
||||||
|
KeyCode keycode = XKeysymToKeycode(display, XK_grave);
|
||||||
|
if (!keycode) {
|
||||||
|
qWarning() << "[AppCore] 未找到波浪号键的键码";
|
||||||
|
XCloseDisplay(display);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_hotkeyCode = keycode;
|
||||||
|
|
||||||
|
XGrabKey(display, keycode, AnyModifier, DefaultRootWindow(display),
|
||||||
|
True, GrabModeAsync, GrabModeAsync);
|
||||||
|
XCloseDisplay(display);
|
||||||
|
|
||||||
|
// debounce 定时器:KeyRelease 后等待 50ms,若没有新的 KeyPress 再停
|
||||||
|
m_pttDebounce = new QTimer(this);
|
||||||
|
m_pttDebounce->setSingleShot(true);
|
||||||
|
connect(m_pttDebounce, &QTimer::timeout, this, &AppCore::onDebounceStop);
|
||||||
|
|
||||||
|
qApp->installNativeEventFilter(this);
|
||||||
|
qDebug() << "[AppCore] Linux 全局 PTT 热键已注册 (键码:" << m_hotkeyCode << ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppCore::cleanupGlobalHotkey() {
|
||||||
|
qApp->removeNativeEventFilter(this);
|
||||||
|
if (m_pttDebounce) {
|
||||||
|
m_pttDebounce->stop();
|
||||||
|
delete m_pttDebounce;
|
||||||
|
m_pttDebounce = nullptr;
|
||||||
|
}
|
||||||
|
Display *display = XOpenDisplay(nullptr);
|
||||||
|
if (display) {
|
||||||
|
XUngrabKey(display, m_hotkeyCode, AnyModifier, DefaultRootWindow(display));
|
||||||
|
XCloseDisplay(display);
|
||||||
|
}
|
||||||
|
m_hotkeyCode = 0;
|
||||||
|
qDebug() << "[AppCore] Linux 全局 PTT 热键已卸载";
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppCore::onDebounceStop() {
|
||||||
|
if (m_isPttDown) {
|
||||||
|
m_isPttDown = false;
|
||||||
|
stopPttRecording();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppCore::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) {
|
||||||
|
if (eventType != "xcb_generic_event_t")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto *data = static_cast<const uint8_t *>(message);
|
||||||
|
uint8_t type = data[0] & 0x7f;
|
||||||
|
|
||||||
|
if ((type == 2 || type == 3) && data[1] == static_cast<uint8_t>(m_hotkeyCode)) {
|
||||||
|
if (type == 2) { // KeyPress
|
||||||
|
m_pttDebounce->stop(); // 取消待决的停止
|
||||||
|
if (!m_isPttDown) {
|
||||||
|
m_isPttDown = true;
|
||||||
|
startPttRecording();
|
||||||
|
}
|
||||||
|
} else { // KeyRelease
|
||||||
|
// 不立即停,等待 50ms 确认没有连发 KeyPress
|
||||||
|
if (m_isPttDown)
|
||||||
|
m_pttDebounce->start(50);
|
||||||
|
}
|
||||||
|
if (result) *result = 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== macOS (CGEventTap) =====================
|
||||||
|
#elif defined(Q_OS_MACOS)
|
||||||
|
static CGEventRef pttEventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
|
||||||
|
auto *core = static_cast<AppCore *>(refcon);
|
||||||
|
|
||||||
|
if (type == kCGEventTapDisabledByTimeout) {
|
||||||
|
if (core && core->m_eventTap)
|
||||||
|
CGEventTapEnable(static_cast<CFMachPortRef>(core->m_eventTap), true);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
if (type != kCGEventKeyDown && type != kCGEventKeyUp)
|
||||||
|
return event;
|
||||||
|
|
||||||
|
CGKeyCode kc = static_cast<CGKeyCode>(CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
|
||||||
|
if (kc != core->m_hotkeyCode)
|
||||||
|
return event;
|
||||||
|
|
||||||
|
if (type == kCGEventKeyDown && !core->m_isPttDown) {
|
||||||
|
core->m_isPttDown = true;
|
||||||
|
core->startPttRecording();
|
||||||
|
} else if (type == kCGEventKeyUp && core->m_isPttDown) {
|
||||||
|
core->m_isPttDown = false;
|
||||||
|
core->stopPttRecording();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppCore::setupGlobalHotkey() {
|
||||||
|
CFMachPortRef tap = CGEventTapCreate(
|
||||||
|
kCGHIDEventTap,
|
||||||
|
kCGHeadInsertEventTap,
|
||||||
|
kCGEventTapOptionDefault,
|
||||||
|
CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp),
|
||||||
|
&pttEventTapCallback,
|
||||||
|
this);
|
||||||
|
|
||||||
|
if (!tap) {
|
||||||
|
qWarning() << "[AppCore] macOS PTT 热键创建失败 (需要辅助功能权限)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_eventTap = tap;
|
||||||
|
CFRunLoopSourceRef src = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0);
|
||||||
|
m_runLoopSource = src;
|
||||||
|
CFRunLoopAddSource(CFRunLoopGetCurrent(), src, kCFRunLoopCommonModes);
|
||||||
|
CGEventTapEnable(tap, true);
|
||||||
|
qDebug() << "[AppCore] macOS 全局 PTT 热键已注册 (键码:" << m_hotkeyCode << ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppCore::cleanupGlobalHotkey() {
|
||||||
|
if (m_eventTap) {
|
||||||
|
auto *tap = static_cast<CFMachPortRef>(m_eventTap);
|
||||||
|
CGEventTapEnable(tap, false);
|
||||||
|
if (m_runLoopSource) {
|
||||||
|
auto *src = static_cast<CFRunLoopSourceRef>(m_runLoopSource);
|
||||||
|
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), src, kCFRunLoopCommonModes);
|
||||||
|
CFRelease(src);
|
||||||
|
}
|
||||||
|
CFRelease(tap);
|
||||||
|
m_eventTap = nullptr;
|
||||||
|
m_runLoopSource = nullptr;
|
||||||
|
}
|
||||||
|
qDebug() << "[AppCore] macOS 全局 PTT 热键已卸载";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user