Document desktop PTT chords and portal lifecycle

This commit is contained in:
evgeny committed 2026-10-27 13:54:55 +01:00
1 parent e9391c9790
commit 5c2ba74e3e
3 files changed
+88 -38

No files matched your search

+34 -13
View File
@@ -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 хранится отдельно.
};
+13 -3
View File
@@ -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;
+41 -22
View File
@@ -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.
};