Document desktop PTT chords and portal lifecycle
This commit is contained in:
1 parent
e9391c9790
commit
5c2ba74e3e
3 files changed
+88
-38
No files matched your search
@@ -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<PttKeyChord> m_chords;
|
||||
PttKeyChord m_held;
|
||||
bool m_capturing = false;
|
||||
bool m_captureArmed = false;
|
||||
bool m_talking = false;
|
||||
QList<PttKeyChord> m_chords; // Дедуплицированные активные физические сочетания.
|
||||
PttKeyChord m_held; // Последний снимок ОС, не конфигурация сочетания.
|
||||
bool m_capturing = false; // Идёт ручная запись вместо вычисления hotkey-PTT.
|
||||
bool m_captureArmed = false; // Исходные клавиши полностью отпущены, можно выдавать keysChanged.
|
||||
bool m_talking = false; // Последний hotkey-PTT физического опроса; состояние portal хранится отдельно.
|
||||
};
|
||||
@@ -1,14 +1,20 @@
|
||||
/* Значения физических сочетаний Windows/X11, не Qt keycodes и не Wayland portal shortcuts.
|
||||
* Коды ОС 8..255 зависят от платформы; левые/правые модификаторы различаются.
|
||||
* encode/decode задают формат одного сочетания для ui_state; захват строится из полных снимков удерживаемых клавиш. */
|
||||
#pragma once
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <algorithm>
|
||||
|
||||
// Физические коды клавиш ОС: левый и правый модификаторы — отдельные элементы.
|
||||
/* Множество одновременно нужных кодов, пустое означает отсутствие сочетания. */
|
||||
using PttKeyChord = QSet<int>;
|
||||
/* 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;
|
||||
|
||||
@@ -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 <QObject>
|
||||
@@ -8,58 +12,73 @@
|
||||
#include <QTimer>
|
||||
#include <QVariantMap>
|
||||
|
||||
/* D-Bus структура (sa{sv}): id сочетания и свойства, включая отображаемый trigger_description. */
|
||||
struct PttPortalShortcut { QString id; QVariantMap properties; };
|
||||
using PttPortalShortcuts = QList<PttPortalShortcut>;
|
||||
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<QString> 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<QString> m_active; // Удерживаемые ptt-ID текущей сессии.
|
||||
QTimer m_requestTimer; // Однократный предел ожидания Create/List Response.
|
||||
bool m_configureRequested = false, m_inhibited = false, m_talking = false; // Запрос назначения, запрет, последний talking.
|
||||
};
|
||||
Reference in new issue
Block a user