diff --git a/CMakeLists.txt b/CMakeLists.txt index 0dd85ca..0f0881b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC 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 if(NOT CMAKE_BUILD_TYPE) @@ -183,6 +183,16 @@ target_link_libraries(${PROJECT_NAME} 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} PRIVATE diff --git a/src/Core/Inc/AppCore.h b/src/Core/Inc/AppCore.h index 106d238..17e8c23 100644 --- a/src/Core/Inc/AppCore.h +++ b/src/Core/Inc/AppCore.h @@ -12,12 +12,22 @@ #include #include #include +#ifdef Q_OS_WIN +#include +#endif +#ifdef Q_OS_LINUX +#include +#endif #include "serialportmanager.h" class DeviceTcpServer; class DeviceWebSocketServer; -class AppCore final : public QObject { +class AppCore final : public QObject +#ifdef Q_OS_LINUX + , public QAbstractNativeEventFilter +#endif +{ Q_OBJECT Q_DISABLE_COPY(AppCore) @@ -44,5 +54,35 @@ public: public: 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 }; \ No newline at end of file diff --git a/src/Core/Src/AppCore.cpp b/src/Core/Src/AppCore.cpp index 9a62528..d03be26 100644 --- a/src/Core/Src/AppCore.cpp +++ b/src/Core/Src/AppCore.cpp @@ -4,6 +4,8 @@ #include "AppCore.h" #include +#include +#include #include "AudioDataHandle.h" #include "AutoAgentHandle.h" @@ -15,10 +17,26 @@ #include "websocketmanager.h" #include "DeviceTcpServer.h" #include "DeviceWebSocketServer.h" + +#ifdef Q_OS_LINUX +#include +#include +#include +#endif + +#ifdef Q_OS_MACOS +#include +#include +#endif + // 初始化静态成员 QScopedPointer AppCore::m_instance; QMutex AppCore::m_mutex; +#ifdef Q_OS_WIN +static AppCore *g_hotkeyInstance = nullptr; +#endif + // 单例实现 (QScopedPointer + Mutex) AppCore* AppCore::getInstance() { @@ -81,10 +99,13 @@ AppCore::AppCore(QObject *parent) : QObject(parent) // 连接必要的信号 connect(AudioInput::getInstance(), &AudioInput::recordingFinished_Byte, this, &AppCore::onRecordingFinished_Byte); + + setupGlobalHotkey(); } AppCore::~AppCore() { + cleanupGlobalHotkey(); if (m_deviceTcpServer) m_deviceTcpServer->stop(); if (m_deviceWsServer) m_deviceWsServer->stop(); @@ -118,3 +139,209 @@ void AppCore::onRecordingFinished_Byte(const QByteArray &wavData) { 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(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(message); + uint8_t type = data[0] & 0x7f; + + if ((type == 2 || type == 3) && data[1] == static_cast(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(refcon); + + if (type == kCGEventTapDisabledByTimeout) { + if (core && core->m_eventTap) + CGEventTapEnable(static_cast(core->m_eventTap), true); + return event; + } + if (type != kCGEventKeyDown && type != kCGEventKeyUp) + return event; + + CGKeyCode kc = static_cast(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(m_eventTap); + CGEventTapEnable(tap, false); + if (m_runLoopSource) { + auto *src = static_cast(m_runLoopSource); + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), src, kCFRunLoopCommonModes); + CFRelease(src); + } + CFRelease(tap); + m_eventTap = nullptr; + m_runLoopSource = nullptr; + } + qDebug() << "[AppCore] macOS 全局 PTT 热键已卸载"; +} +#endif +