You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
252 lines
8.4 KiB
252 lines
8.4 KiB
#include "ptt_hotkey_manager.h" |
|
#include "radio_audio_engine.h" |
|
#include "../db/db_manager.h" |
|
#include "../../lib/debug_config.h" |
|
|
|
#include <QGuiApplication> |
|
#include <QWidget> |
|
#ifdef UTUN_PTT_PORTAL |
|
#include "ptt_portal.h" |
|
#endif |
|
#ifdef Q_OS_WIN |
|
#include <qt_windows.h> |
|
#elif defined(Q_OS_LINUX) |
|
#include <X11/Xlib.h> |
|
#include <X11/keysym.h> |
|
#include <X11/XKBlib.h> |
|
#endif |
|
|
|
PttHotkeyManager* PttHotkeyManager::instance() { |
|
static auto* manager = new PttHotkeyManager(); |
|
return manager; |
|
} |
|
|
|
PttHotkeyManager::PttHotkeyManager() : QObject(qApp) { |
|
#ifdef Q_OS_LINUX |
|
if (isSupported() && !usesPortal()) m_display = XOpenDisplay(nullptr); |
|
if (isSupported() && !usesPortal() && !m_display) DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "PTT: cannot open X11 display"); |
|
#endif |
|
#ifdef UTUN_PTT_PORTAL |
|
if (usesPortal()) { |
|
m_portal = new PttPortal(this); |
|
connect(m_portal, &PttPortal::talkingChanged, this, [](bool talking) { |
|
RadioAudioEngine::instance()->setHotkeyTalking(talking); |
|
}); |
|
connect(m_portal, &PttPortal::descriptionsChanged, this, &PttHotkeyManager::portalChanged); |
|
connect(m_portal, &PttPortal::statusChanged, this, &PttHotkeyManager::portalChanged); |
|
} |
|
#endif |
|
m_timer.setInterval(10); |
|
m_timer.setTimerType(Qt::PreciseTimer); |
|
connect(&m_timer, &QTimer::timeout, this, &PttHotkeyManager::poll); |
|
} |
|
|
|
PttHotkeyManager::~PttHotkeyManager() { |
|
m_timer.stop(); |
|
#ifdef Q_OS_LINUX |
|
if (m_display) XCloseDisplay(static_cast<Display*>(m_display)); |
|
#endif |
|
} |
|
|
|
bool PttHotkeyManager::isSupported() { |
|
#ifdef Q_OS_WIN |
|
return true; |
|
#elif defined(Q_OS_LINUX) |
|
return usesPortal() || QGuiApplication::platformName() == "xcb"; |
|
#else |
|
return false; |
|
#endif |
|
} |
|
|
|
void PttHotkeyManager::init(DbManager* db) { |
|
m_db = db; |
|
#ifdef UTUN_PTT_PORTAL |
|
if (m_portal) { m_portal->initialize(); return; } |
|
#endif |
|
reload(); |
|
if (isSupported()) { |
|
#ifdef Q_OS_LINUX |
|
if (!m_display) return; |
|
#endif |
|
m_timer.start(); |
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "PTT: global keyboard polling enabled interval=10ms"); |
|
} else DEBUG_WARN(DEBUG_CATEGORY_RADIO, "PTT: global keyboard unavailable platform=%s", qPrintable(QGuiApplication::platformName())); |
|
} |
|
|
|
QStringList PttHotkeyManager::hotkeys() const { |
|
QStringList values; |
|
for (const auto& chord : m_chords) values << pttEncodeChord(chord); |
|
return values; |
|
} |
|
|
|
void PttHotkeyManager::setHotkeys(const QStringList& values) { |
|
if (usesPortal()) return; |
|
RadioAudioEngine::instance()->setHotkeyTalking(false); |
|
m_talking = false; |
|
m_chords.clear(); |
|
for (const auto& value : values) { |
|
auto chord = pttDecodeChord(value); |
|
if (value.isEmpty()) continue; |
|
if (chord.isEmpty()) { |
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "PTT: invalid key chord '%s'", qPrintable(value)); |
|
continue; |
|
} |
|
if (!m_chords.contains(chord)) m_chords << chord; |
|
} |
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "PTT: configured %d key chords", int(m_chords.size())); |
|
} |
|
|
|
void PttHotkeyManager::reload() { |
|
setHotkeys(m_db && m_db->isOpen() ? m_db->getUiState("radio_ptt_hotkeys").split('|', Qt::SkipEmptyParts) : QStringList()); |
|
} |
|
|
|
bool PttHotkeyManager::readKeys(PttKeyChord& held) const { |
|
#ifdef Q_OS_WIN |
|
for (int key = 8; key < 256; key++) { |
|
// Общие VK_SHIFT/CONTROL/MENU дублируют левые/правые — исключаем их. |
|
if (key == VK_SHIFT || key == VK_CONTROL || key == VK_MENU) continue; |
|
if (GetAsyncKeyState(key) & 0x8000) held.insert(key); |
|
} |
|
return true; |
|
#elif defined(Q_OS_LINUX) |
|
if (!m_display) return false; |
|
char keys[32]; |
|
if (!XQueryKeymap(static_cast<Display*>(m_display), keys)) return false; |
|
for (int key = 8; key < 256; key++) if ((unsigned char)keys[key / 8] & (1u << (key % 8))) held.insert(key); |
|
return true; |
|
#else |
|
return false; |
|
#endif |
|
} |
|
|
|
void PttHotkeyManager::poll() { |
|
PttKeyChord held; |
|
if (!readKeys(held)) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "PTT: keyboard query failed; transmission disabled"); |
|
m_timer.stop(); |
|
RadioAudioEngine::instance()->setHotkeyTalking(false); |
|
m_talking = false; |
|
return; |
|
} |
|
if (m_capturing && !m_captureArmed && held.isEmpty()) m_captureArmed = true; |
|
bool changed = held != m_held; |
|
m_held = held; |
|
bool talking = false; |
|
if (!m_capturing && RadioAudioEngine::instance()->isActive()) { |
|
for (const auto& chord : m_chords) if (pttChordHeld(chord, held)) { talking = true; break; } |
|
} |
|
if (talking != m_talking) { |
|
m_talking = talking; |
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "PTT: global transmission %s keys=%s", talking ? "started" : "stopped", |
|
qPrintable(describe(held))); |
|
} |
|
RadioAudioEngine::instance()->setHotkeyTalking(talking); |
|
if (m_capturing && m_captureArmed && changed) emit keysChanged(held); |
|
} |
|
|
|
bool PttHotkeyManager::beginCapture() { |
|
if (!m_timer.isActive() || m_capturing) { |
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "PTT: capture unavailable polling=%d capturing=%d", m_timer.isActive(), m_capturing); |
|
return false; |
|
} |
|
m_capturing = true; |
|
PttKeyChord held; |
|
if (!readKeys(held)) { |
|
m_capturing = false; |
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "PTT: cannot read keys when starting capture"); |
|
return false; |
|
} |
|
m_captureArmed = held.isEmpty(); // ждём отпускания клавиши, которой нажали кнопку |
|
m_talking = false; |
|
RadioAudioEngine::instance()->setHotkeyTalking(false); |
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "PTT: key capture started"); |
|
return true; |
|
} |
|
|
|
void PttHotkeyManager::endCapture() { |
|
if (!m_capturing) return; |
|
m_capturing = false; |
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "PTT: key capture finished"); |
|
} |
|
|
|
QString PttHotkeyManager::keyName(int key) const { |
|
#ifdef Q_OS_WIN |
|
switch (key) { |
|
case VK_LSHIFT: return "ShiftL"; case VK_RSHIFT: return "ShiftR"; |
|
case VK_LCONTROL: return "CtrlL"; case VK_RCONTROL: return "CtrlR"; |
|
case VK_LMENU: return "AltL"; case VK_RMENU: return "AltR"; |
|
case VK_LWIN: return "WinL"; case VK_RWIN: return "WinR"; |
|
} |
|
UINT scan = MapVirtualKeyW(key, MAPVK_VK_TO_VSC_EX); |
|
LONG param = LONG((scan & 0xff) << 16); |
|
if (scan & 0xff00) param |= 1 << 24; |
|
wchar_t name[128]; |
|
if (GetKeyNameTextW(param, name, 128)) return QString::fromWCharArray(name); |
|
#elif defined(Q_OS_LINUX) |
|
if (m_display) { |
|
KeySym sym = XkbKeycodeToKeysym(static_cast<Display*>(m_display), key, 0, 0); |
|
switch (sym) { |
|
case XK_Shift_L: return "ShiftL"; case XK_Shift_R: return "ShiftR"; |
|
case XK_Control_L: return "CtrlL"; case XK_Control_R: return "CtrlR"; |
|
case XK_Alt_L: return "AltL"; case XK_Alt_R: return "AltR"; |
|
case XK_Super_L: return "WinL"; case XK_Super_R: return "WinR"; |
|
case XK_space: return "Space"; |
|
} |
|
const char* name = XKeysymToString(sym); |
|
if (name) return QString::fromLatin1(name); |
|
} |
|
#endif |
|
return QString("Key %1").arg(key); |
|
} |
|
|
|
QString PttHotkeyManager::describe(const PttKeyChord& chord) const { |
|
auto keys = chord.values(); |
|
std::sort(keys.begin(), keys.end()); |
|
QStringList names; |
|
for (int key : keys) names << keyName(key); |
|
return names.isEmpty() ? QStringLiteral("Не задано") : names.join(" + "); |
|
} |
|
|
|
bool PttHotkeyManager::usesPortal() { |
|
#ifdef UTUN_PTT_PORTAL |
|
return qEnvironmentVariable("XDG_SESSION_TYPE") == "wayland" || QGuiApplication::platformName().startsWith("wayland"); |
|
#else |
|
return false; |
|
#endif |
|
} |
|
void PttHotkeyManager::configurePortal(QWidget* parentWindow) { |
|
#ifdef UTUN_PTT_PORTAL |
|
if (!m_portal) return; |
|
QString id; |
|
if (parentWindow && QGuiApplication::platformName() == "xcb") |
|
id = "x11:" + QString::number(qulonglong(parentWindow->window()->winId()), 16); |
|
m_portal->configure(id); |
|
#else |
|
Q_UNUSED(parentWindow); |
|
#endif |
|
} |
|
QStringList PttHotkeyManager::portalDescriptions() const { |
|
#ifdef UTUN_PTT_PORTAL |
|
if (m_portal) return m_portal->descriptions(); |
|
#endif |
|
return {}; |
|
} |
|
QString PttHotkeyManager::portalStatus() const { |
|
#ifdef UTUN_PTT_PORTAL |
|
if (m_portal) return m_portal->status(); |
|
#endif |
|
return {}; |
|
} |
|
bool PttHotkeyManager::portalBusy() const { |
|
#ifdef UTUN_PTT_PORTAL |
|
if (m_portal) return m_portal->busy(); |
|
#endif |
|
return false; |
|
} |
|
void PttHotkeyManager::inhibitPortal(bool inhibited) { |
|
#ifdef UTUN_PTT_PORTAL |
|
if (m_portal) m_portal->setInhibited(inhibited); |
|
#else |
|
Q_UNUSED(inhibited); |
|
#endif |
|
}
|
|
|