From 497ecd5d159f2596a90d4da08b254f0d2bba9556 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 31 Oct 2026 17:17:31 +0100 Subject: [PATCH] Register one Wayland PTT shortcut and reset latches on session changes --- tools/chatgui/src/ptt_portal.cpp | 43 +++++++++++++++++--------------- tools/chatgui/src/ptt_portal.h | 31 ++++++++++++----------- 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/tools/chatgui/src/ptt_portal.cpp b/tools/chatgui/src/ptt_portal.cpp index a4369173..d50f0e10 100644 --- a/tools/chatgui/src/ptt_portal.cpp +++ b/tools/chatgui/src/ptt_portal.cpp @@ -123,11 +123,11 @@ void PttPortal::response(uint code, const QVariantMap& results, const QDBusMessa } else if (state == State::Listing) { auto shortcuts = qdbus_cast(results.value("shortcuts")); updateDescriptions(shortcuts); - QSet 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"); } diff --git a/tools/chatgui/src/ptt_portal.h b/tools/chatgui/src/ptt_portal.h index 57680079..477de368 100644 --- a/tools/chatgui/src/ptt_portal.h +++ b/tools/chatgui/src/ptt_portal.h @@ -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 #include #include #include -#include #include #include @@ -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 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. };