diff --git a/tools/chatgui/src/ptt_hotkey_manager.h b/tools/chatgui/src/ptt_hotkey_manager.h index cec97084..7db2cbdf 100644 --- a/tools/chatgui/src/ptt_hotkey_manager.h +++ b/tools/chatgui/src/ptt_hotkey_manager.h @@ -1,3 +1,7 @@ +/* Глобальный PTT: Windows/X11 опрашивают физические клавиши каждые 10 мс, Wayland использует системный портал. + * Все методы/сигналы — GUI-поток, singleton принадлежит qApp; init вызывается после открытия БД. + * DbManager заимствуется. Физические сочетания читаются из ui_state radio_ptt_hotkeys; сохранение выполняет вызывающий. + * Wayland-комбинациями владеет портал. Менеджер меняет только hotkey-владельца PTT, не кнопку и не автоматическую передачу. */ #pragma once #include "ptt_key_chord.h" @@ -8,47 +12,64 @@ class DbManager; class PttPortal; class QWidget; -// Глобальный PTT: Windows/X11 — состояние физических клавиш; Wayland — системный портал. -// Физические комбинации хранит ui_state radio_ptt_hotkeys, Wayland-комбинации — портал. class PttHotkeyManager : public QObject { Q_OBJECT public: + /* Заимствованный singleton на время жизни qApp; первый вызов должен быть из GUI-потока. */ static PttHotkeyManager* instance(); + /* Запоминает db; запускает портал либо reload/опрос клавиатуры. Наличие поддержки не гарантирует доступ к backend. */ void init(DbManager* db); + /* Активные физические сочетания в формате pttEncodeChord; пустые/повторяющиеся позиции не сохраняются. */ QStringList hotkeys() const; + /* Применяет физические сочетания в памяти, снимая текущий hotkey-PTT; пропускает пустые/невалидные/дубли. + * В режиме портала ничего не делает; БД не меняет. */ void setHotkeys(const QStringList& chords); + /* Загружает radio_ptt_hotkeys (сочетания разделены '|'); без открытой БД применяет пустой список. */ void reload(); + /* Поддержка платформы: Windows, Linux/xcb либо выбранный portal; не проверяет доступность сервиса/устройства. */ static bool isSupported(); + /* UTUN_PTT_PORTAL и Wayland по XDG_SESSION_TYPE либо Qt platformName; иначе false. */ static bool usesPortal(); + /* Открывает системное назначение при наличии portal; parentWindow заимствуется на вызов для X11 window ID. */ void configurePortal(QWidget* parentWindow); + /* Описания ptt1..ptt3 от портала; без portal пустой список. */ QStringList portalDescriptions() const; + /* Локализованный статус portal либо пустая строка. */ QString portalStatus() const; + /* Идёт инициализация/запрос назначения; не включает открытое системное окно ConfigureShortcuts. */ bool portalBusy() const; + /* При true снимает portal-PTT и игнорирует новые Activated; false не восстанавливает прежнее удержание. */ void inhibitPortal(bool inhibited); + /* Читаемые имена кодов в отсортированном порядке, пустой chord — «Not set»; сочетание не изменяет. */ QString describe(const PttKeyChord& chord) const; + /* Начинает запись только при работающем физическом опросе и отсутствии другой записи; false при отказе. + * Снимает hotkey-PTT и ждёт полного отпускания исходных клавиш перед keysChanged. Для Wayland не используется. */ bool beginCapture(); + /* Завершает запись без сохранения; обычный hotkey-PTT снова вычисляется следующим опросом. */ void endCapture(); signals: + /* Изменились описания или статус portal; перечитать portalDescriptions/portalStatus/portalBusy. */ void portalChanged(); + /* Полный снимок физических клавиш только при записи, после её вооружения и при изменении снимка. */ void keysChanged(const PttKeyChord& held); private: PttHotkeyManager(); ~PttHotkeyManager() override; - void pollKeys(); - bool readKeys(PttKeyChord& held) const; - QString keyName(int key) const; + void pollKeys(); /* Обновляет hotkey-PTT/запись; ошибка чтения отключает PTT и останавливает таймер. */ + bool readKeys(PttKeyChord& held) const; /* Добавляет нажатые коды в изначально пустой held; false при отказе backend. */ + QString keyName(int key) const; /* Имя кода ОС либо локализованный запасной вариант Key N. */ #ifdef UTUN_PTT_PORTAL - PttPortal* m_portal = nullptr; + PttPortal* m_portal = nullptr; /* Дочерняя portal-сессия, создаётся только для выбранного Wayland-режима. */ #endif - DbManager* m_db = nullptr; - QTimer m_timer; + DbManager* m_db = nullptr; // Заимствованная БД, должна быть доступна при reload. + QTimer m_timer; // Физический опрос, 10 мс; portal использует сигналы без этого таймера. void* m_display = nullptr; // собственное X11-соединение, только GUI-поток - QList m_chords; - PttKeyChord m_held; - bool m_capturing = false; - bool m_captureArmed = false; - bool m_talking = false; + QList m_chords; // Дедуплицированные активные физические сочетания. + PttKeyChord m_held; // Последний снимок ОС, не конфигурация сочетания. + bool m_capturing = false; // Идёт ручная запись вместо вычисления hotkey-PTT. + bool m_captureArmed = false; // Исходные клавиши полностью отпущены, можно выдавать keysChanged. + bool m_talking = false; // Последний hotkey-PTT физического опроса; состояние portal хранится отдельно. }; diff --git a/tools/chatgui/src/ptt_key_chord.h b/tools/chatgui/src/ptt_key_chord.h index c623ae67..0d081b93 100644 --- a/tools/chatgui/src/ptt_key_chord.h +++ b/tools/chatgui/src/ptt_key_chord.h @@ -1,14 +1,20 @@ +/* Значения физических сочетаний Windows/X11, не Qt keycodes и не Wayland portal shortcuts. + * Коды ОС 8..255 зависят от платформы; левые/правые модификаторы различаются. + * encode/decode задают формат одного сочетания для ui_state; захват строится из полных снимков удерживаемых клавиш. */ #pragma once #include #include #include #include -// Физические коды клавиш ОС: левый и правый модификаторы — отдельные элементы. +/* Множество одновременно нужных кодов, пустое означает отсутствие сочетания. */ using PttKeyChord = QSet; +/* true, если непустой chord целиком входит в held; дополнительные удерживаемые клавиши допустимы. */ inline bool pttChordHeld(const PttKeyChord& chord, const PttKeyChord& held) { return !chord.isEmpty() && (chord - held).isEmpty(); } +/* Сортирует коды по возрастанию и соединяет десятичные числа запятыми; пустое множество даёт пустую строку. + * Диапазон кодов здесь не проверяется; передавать допустимый PttKeyChord. */ inline QString pttEncodeChord(const PttKeyChord& chord) { auto keys = chord.values(); std::sort(keys.begin(), keys.end()); @@ -16,6 +22,7 @@ inline QString pttEncodeChord(const PttKeyChord& chord) { for (int key : keys) values << QString::number(key); return values.join(','); } +/* Разбирает целые 8..255, удаляя дубликаты; пустой ввод либо ошибка любого элемента возвращают пустое множество. */ inline PttKeyChord pttDecodeChord(const QString& value) { PttKeyChord chord; if (value.isEmpty()) return chord; @@ -28,9 +35,12 @@ inline PttKeyChord pttDecodeChord(const QString& value) { return chord; } -// Сохраняем реально одновременное сочетание, а не объединение последовательных нажатий. +/* Сохраняет самый большой реально одновременный снимок, при равной мощности — последний. + * Создать/сбросить объект перед новой записью; последовательные нажатия не объединяются. */ struct PttChordCapture { - PttKeyChord candidate; + PttKeyChord candidate; /* Выбранный снимок; частичное отпускание его не сокращает. */ + /* true только при полном отпускании после непустого candidate: запись можно завершить. + * Сам candidate после true не очищается; передавать вызовы в порядке опроса клавиатуры. */ bool observe(const PttKeyChord& held) { if (held.isEmpty()) return !candidate.isEmpty(); if (held.size() >= candidate.size()) candidate = held; diff --git a/tools/chatgui/src/ptt_portal.h b/tools/chatgui/src/ptt_portal.h index 29cdf873..57680079 100644 --- a/tools/chatgui/src/ptt_portal.h +++ b/tools/chatgui/src/ptt_portal.h @@ -1,3 +1,7 @@ +/* Wayland PTT через org.freedesktop.portal.GlobalShortcuts v2+, все вызовы/сигналы — поток Qt event loop владельца. + * Портал хранит назначения ptt1..ptt3, объект владеет D-Bus-сессией. initialize не открывает первое назначение без configure. + * Create/List/Bind выполняются последовательно; Response подключается до вызова метода. ConfigureShortcuts — отдельный вызов. + * Несколько активных shortcut объединяются в один talking; inhibited и ошибка/закрытие сессии снимают удержания. */ #pragma once #include @@ -8,58 +12,73 @@ #include #include +/* D-Bus структура (sa{sv}): id сочетания и свойства, включая отображаемый trigger_description. */ struct PttPortalShortcut { QString id; QVariantMap properties; }; using PttPortalShortcuts = QList; Q_DECLARE_METATYPE(PttPortalShortcut) Q_DECLARE_METATYPE(PttPortalShortcuts) +/* Сериализуют/читают одну структуру в arg и возвращают тот же аргумент; валидацию ptt-ID выполняет PttPortal. */ QDBusArgument& operator<<(QDBusArgument& arg, const PttPortalShortcut& shortcut); const QDBusArgument& operator>>(const QDBusArgument& arg, PttPortalShortcut& shortcut); -// Wayland PTT: портал владеет сочетаниями и хранит их; приложение владеет сессией. -// Один запрос за раз. Подписка на Response устанавливается до вызова метода. class PttPortal : public QObject { Q_OBJECT public: + /* Подключает сигналы session bus; sessionToken — стабильный token идентичности сессии, не handle запроса. */ explicit PttPortal(QObject* parent = nullptr, const QString& sessionToken = "utun_vibechat_ptt"); + /* Посылает асинхронные Close активному запросу/сессии, не ждёт ответов. */ ~PttPortal() override; + /* Проверяет версию, создаёт сессию, читает назначения и связывает известные сочетания. + * При busy/Ready/Unbound ничего не делает; после Failed допускает явный повтор. */ void initialize(); + /* Запрашивает системное назначение ptt1..ptt3; parentWindow — строковый portal window ID либо пустая строка. + * В Ready открывает ConfigureShortcuts, иначе инициирует/продолжает путь к BindShortcuts. Возвращается сразу. */ void configure(const QString& parentWindow); + /* true очищает активные удержания и игнорирует Activated; false требует нового Activated для возобновления. */ void setInhibited(bool inhibited); + /* Три позиции ptt1..ptt3; пустая строка — нет trigger_description, список может обновиться после системного диалога. */ QStringList descriptions() const { return m_descriptions; } + /* Локализованный пользовательский статус; технические причины отказа находятся в категории radio. */ QString status() const { return m_status; } + /* Checking/Creating/Listing/Binding; не означает, что внешнее окно ConfigureShortcuts закрыто. */ bool busy() const; signals: + /* Перечитать descriptions; выдаётся и при сбросе после ошибки. */ void descriptionsChanged(); void statusChanged(); + /* Изменилось наличие хотя бы одного удержания ptt1..ptt3 с учётом inhibited, независимо от доступности рации. */ void talkingChanged(bool talking); private slots: + /* Принимает только Response текущего пути, переводит Create -> List -> Bind -> Ready либо завершает с ошибкой. */ void response(uint code, const QVariantMap& results, const QDBusMessage& message); + /* Сигналы фильтруются по текущей сессии/ptt-ID; timestamp/options для удержания не используются. */ void activated(const QDBusObjectPath& session, const QString& id, qulonglong timestamp, const QVariantMap& options); void deactivated(const QDBusObjectPath& session, const QString& id, qulonglong timestamp, const QVariantMap& options); - void shortcutsChanged(const QDBusObjectPath& session, const PttPortalShortcuts& shortcuts); - void sessionClosed(); + void shortcutsChanged(const QDBusObjectPath& session, const PttPortalShortcuts& shortcuts); // Снимает удержания и обновляет подписи. + void sessionClosed(); // Переводит в Failed через fail. private: - enum class State { Idle, Checking, Creating, Listing, Unbound, Binding, Ready, Failed }; + enum class State { Idle, Checking, Creating, Listing, Unbound, Binding, Ready, Failed }; // Unbound — сессия без привязки. + /* Один portal Request с заранее вычисленным handle; Create/List имеют таймер 30 с, Bind ждёт пользователя без этого таймера. */ void request(const QString& method, const QVariantList& arguments, QVariantMap options); - void bind(); - void openConfiguration(); - void updateDescriptions(const PttPortalShortcuts& shortcuts); - void updateTalking(); - void setStatus(const QString& text); - void fail(const QString& detail); - void disconnectRequest(); - void closeSession(); - static bool isPttId(const QString& id); + void bind(); // Связывает все три ptt-ID; первое назначение может открыть системный диалог. + void openConfiguration(); // Отдельный асинхронный ConfigureShortcuts, не создаёт Request и не меняет busy. + void updateDescriptions(const PttPortalShortcuts& shortcuts); // Полностью заменяет три подписи из trigger_description. + void updateTalking(); // Вычисляет !inhibited && !active.empty, сигнал только при изменении. + void setStatus(const QString& text); // Запоминает text и выдаёт statusChanged. + void fail(const QString& detail); // Лог, Close, сброс удержаний/подписей, Failed и пользовательский статус. + void disconnectRequest(); // Останавливает таймер, отсоединяет Response и очищает путь, без Request.Close. + void closeSession(); // Инвалидирует старые callbacks, посылает Close запросу/сессии, очищает пути. + static bool isPttId(const QString& id); // Ровно ptt1, ptt2 либо ptt3. - uint m_epoch = 0; - bool m_configureAfterBind = false; - State m_state = State::Idle; - QString m_sessionToken, m_session, m_request, m_parentWindow, m_status; - QStringList m_descriptions = {"", "", ""}; - QSet m_active; - QTimer m_requestTimer; - bool m_configureRequested = false, m_inhibited = false, m_talking = false; + uint m_epoch = 0; // Поколение сессии для отбрасывания поздних asyncCall-ответов. + bool m_configureAfterBind = false; // После восстановления трёх сохранённых ID нужно открыть ConfigureShortcuts. + State m_state = State::Idle; // Этап сериализованной инициализации/привязки. + QString m_sessionToken, m_session, m_request, m_parentWindow, m_status; // Token, два D-Bus-пути, window ID и UI-статус. + QStringList m_descriptions = {"", "", ""}; // Подписи в стабильном порядке ptt1..ptt3. + QSet m_active; // Удерживаемые ptt-ID текущей сессии. + QTimer m_requestTimer; // Однократный предел ожидания Create/List Response. + bool m_configureRequested = false, m_inhibited = false, m_talking = false; // Запрос назначения, запрет, последний talking. };