Register one Wayland PTT shortcut and reset latches on session changes

This commit is contained in:
evgeny committed 2026-10-31 17:17:31 +01:00
1 parent 239c4f7a7a
commit 497ecd5d15
2 files changed
+39 -35

No files matched your search

+23 -20
View File
@@ -123,11 +123,11 @@ void PttPortal::response(uint code, const QVariantMap& results, const QDBusMessa
} else if (state == State::Listing) {
auto shortcuts = qdbus_cast<PttPortalShortcuts>(results.value("shortcuts"));
updateDescriptions(shortcuts);
QSet<QString> known;
for (const auto& shortcut : shortcuts) if (isPttId(shortcut.id)) known.insert(shortcut.id);
m_configureAfterBind = known.size() == 3 && m_configureRequested;
bool known = false;
for (const auto& shortcut : shortcuts) if (isPttId(shortcut.id)) known = true;
m_configureAfterBind = known && m_configureRequested;
m_state = State::Unbound;
if (!known.isEmpty() || m_configureRequested) bind();
if (known || m_configureRequested) bind();
else setStatus(QCoreApplication::translate("ChatGui", "Assign a shortcut in system settings."));
} else if (state == State::Binding) {
m_state = State::Ready;
@@ -140,12 +140,10 @@ void PttPortal::response(uint code, const QVariantMap& results, const QDBusMessa
} else fail("unexpected request response");
}
bool PttPortal::isPttId(const QString& id) { return id == "ptt1" || id == "ptt2" || id == "ptt3"; }
bool PttPortal::isPttId(const QString& id) { return id == "ptt"; }
void PttPortal::bind() {
PttPortalShortcuts shortcuts;
for (int i = 1; i <= 3; i++) shortcuts << PttPortalShortcut{QString("ptt%1").arg(i), {{"description", QCoreApplication::translate("ChatGui",
"Radio: transmit %1").arg(i)}}};
PttPortalShortcuts shortcuts{{"ptt", {{"description", QCoreApplication::translate("ChatGui", "Radio: transmit")}}}};
m_state = State::Binding;
setStatus(QCoreApplication::translate("ChatGui", "Assigning global shortcuts…"));
request("BindShortcuts", {QVariant::fromValue(QDBusObjectPath(m_session)), QVariant::fromValue(shortcuts), m_parentWindow}, {});
@@ -174,37 +172,41 @@ void PttPortal::openConfiguration() {
}
void PttPortal::updateDescriptions(const PttPortalShortcuts& shortcuts) {
m_descriptions = {"", "", ""};
m_description.clear();
for (const auto& shortcut : shortcuts) {
if (!isPttId(shortcut.id)) continue;
int index = shortcut.id.right(1).toInt() - 1;
m_descriptions[index] = shortcut.properties.value("trigger_description").toString();
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "PTT portal: %s trigger=%s", qPrintable(shortcut.id), qPrintable(m_descriptions[index]));
m_description = shortcut.properties.value("trigger_description").toString();
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "PTT portal: %s trigger=%s", qPrintable(shortcut.id), qPrintable(m_description));
}
emit descriptionsChanged();
}
void PttPortal::activated(const QDBusObjectPath& session, const QString& id, qulonglong, const QVariantMap&) {
if (session.path() != m_session || m_state != State::Ready || !isPttId(id) || m_inhibited) return;
m_active.insert(id); updateTalking();
m_pressed = true; updateTalking();
}
void PttPortal::deactivated(const QDBusObjectPath& session, const QString& id, qulonglong, const QVariantMap&) {
if (session.path() != m_session || !isPttId(id)) return;
m_active.remove(id); updateTalking();
m_pressed = false; updateTalking();
}
void PttPortal::shortcutsChanged(const QDBusObjectPath& session, const PttPortalShortcuts& shortcuts) {
if (session.path() != m_session) return;
m_active.clear(); updateTalking(); updateDescriptions(shortcuts);
m_pressed = false; updateTalking();
emit resetRequested("portal-shortcut-changed");
updateDescriptions(shortcuts);
}
void PttPortal::setInhibited(bool inhibited) {
m_inhibited = inhibited;
if (inhibited) { m_active.clear(); updateTalking(); }
if (inhibited) {
m_pressed = false; updateTalking();
emit resetRequested("portal-inhibited");
}
}
void PttPortal::updateTalking() {
bool talking = !m_inhibited && !m_active.isEmpty();
bool talking = !m_inhibited && m_pressed;
if (m_talking == talking) return;
m_talking = talking;
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "PTT portal: transmission %s active_shortcuts=%d", talking ? "started" : "stopped", int(m_active.size()));
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "PTT portal: shortcut pressed=%d inhibited=%d", m_pressed, m_inhibited);
emit talkingChanged(talking);
}
@@ -228,10 +230,11 @@ void PttPortal::closeSession() {
void PttPortal::fail(const QString& detail) {
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "PTT portal: %s", qPrintable(detail));
closeSession();
m_active.clear(); updateTalking();
m_pressed = false; updateTalking();
emit resetRequested("portal-failed");
m_configureRequested = m_configureAfterBind = false;
m_state = State::Failed;
m_descriptions = {"", "", ""}; emit descriptionsChanged();
m_description.clear(); emit descriptionsChanged();
setStatus(QCoreApplication::translate("ChatGui", "Global shortcuts unavailable. Try assigning again; see the radio log for details."));
}
void PttPortal::sessionClosed() { fail("portal session closed"); }
+16 -15
View File
@@ -1,14 +1,13 @@
/* Wayland PTT через org.freedesktop.portal.GlobalShortcuts v2+, все вызовы/сигналы — поток Qt event loop владельца.
* Портал хранит назначения ptt1..ptt3, объект владеет D-Bus-сессией. initialize не открывает первое назначение без configure.
* Портал хранит одно назначение ptt, объект владеет D-Bus-сессией. initialize не открывает первое назначение без configure.
* Create/List/Bind выполняются последовательно; Response подключается до вызова метода. ConfigureShortcuts — отдельный вызов.
* Несколько активных shortcut объединяются в один talking; inhibited и ошибка/закрытие сессии снимают удержания. */
* Выдаёт фронты удержания; фиксацией двойного нажатия владеет PttHotkeyManager. Сбои/настройка требуют сброса фиксации. */
#pragma once
#include <QObject>
#include <QDBusArgument>
#include <QDBusObjectPath>
#include <QDBusMessage>
#include <QSet>
#include <QTimer>
#include <QVariantMap>
@@ -31,24 +30,26 @@ public:
/* Проверяет версию, создаёт сессию, читает назначения и связывает известные сочетания.
* При busy/Ready/Unbound ничего не делает; после Failed допускает явный повтор. */
void initialize();
/* Запрашивает системное назначение ptt1..ptt3; parentWindow — строковый portal window ID либо пустая строка.
/* Запрашивает системное назначение ptt; 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; }
/* trigger_description единственного сочетания; пустая строка — назначение отсутствует. */
QString description() const { return m_description; }
/* Локализованный пользовательский статус; технические причины отказа находятся в категории radio. */
QString status() const { return m_status; }
/* Checking/Creating/Listing/Binding; не означает, что внешнее окно ConfigureShortcuts закрыто. */
bool busy() const;
signals:
/* Перечитать descriptions; выдаётся и при сбросе после ошибки. */
/* Перечитать description; выдаётся и при сбросе после ошибки. */
void descriptionsChanged();
void statusChanged();
/* Изменилось наличие хотя бы одного удержания ptt1..ptt3 с учётом inhibited, независимо от доступности рации. */
/* Изменилось удержание ptt с учётом inhibited, независимо от доступности рации. */
void talkingChanged(bool talking);
/* Настройка/ошибка требуют снять также фиксацию и историю кликов; reason — диагностическая причина. */
void resetRequested(const QString& reason);
private slots:
/* Принимает только Response текущего пути, переводит Create -> List -> Bind -> Ready либо завершает с ошибкой. */
@@ -63,22 +64,22 @@ private:
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(); // Связывает все три ptt-ID; первое назначение может открыть системный диалог.
void bind(); // Связывает единственный ptt-ID; первое назначение может открыть системный диалог.
void openConfiguration(); // Отдельный асинхронный ConfigureShortcuts, не создаёт Request и не меняет busy.
void updateDescriptions(const PttPortalShortcuts& shortcuts); // Полностью заменяет три подписи из trigger_description.
void updateTalking(); // Вычисляет !inhibited && !active.empty, сигнал только при изменении.
void updateDescriptions(const PttPortalShortcuts& shortcuts); // Заменяет подпись ptt из trigger_description.
void updateTalking(); // Вычисляет !inhibited && pressed, сигнал только при изменении.
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.
static bool isPttId(const QString& id); // Только ptt.
uint m_epoch = 0; // Поколение сессии для отбрасывания поздних asyncCall-ответов.
bool m_configureAfterBind = false; // После восстановления трёх сохранённых ID нужно открыть ConfigureShortcuts.
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 текущей сессии.
QString m_description; // Подпись единственного сочетания ptt.
bool m_pressed = false; // Удержание ptt текущей сессии.
QTimer m_requestTimer; // Однократный предел ожидания Create/List Response.
bool m_configureRequested = false, m_inhibited = false, m_talking = false; // Запрос назначения, запрет, последний talking.
};