From fa700938667424b0f0a6b11cdeb55a00619c0068 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 3 Oct 2026 13:09:38 +0300 Subject: [PATCH] Add desktop radio panel with device menus and VAD switch --- tools/chatgui/src/radiopanel.cpp | 158 +++++++++++++++++++++++++++++++ tools/chatgui/src/radiopanel.h | 23 +++++ 2 files changed, 181 insertions(+) create mode 100644 tools/chatgui/src/radiopanel.cpp create mode 100644 tools/chatgui/src/radiopanel.h diff --git a/tools/chatgui/src/radiopanel.cpp b/tools/chatgui/src/radiopanel.cpp new file mode 100644 index 00000000..870d8ab5 --- /dev/null +++ b/tools/chatgui/src/radiopanel.cpp @@ -0,0 +1,158 @@ +#include "radiopanel.h" +#include "radio_audio_engine.h" +#include "sound_manager.h" +#include "../transport/gui_bridge.h" +extern "C" { +#include "chat/chat_core.h" +#include "../../lib/debug_config.h" +#include "../../lib/mem.h" +} +#include +#include +#include +#include +#include +#include +#include +#include + +RadioPanel::RadioPanel(QWidget* parent) : QWidget(parent) { + setObjectName(QStringLiteral("radioPanel")); + auto* row = new QHBoxLayout(this); + row->setContentsMargins(10, 6, 10, 6); + row->setSpacing(8); + m_input = new QToolButton(this); + m_output = new QToolButton(this); + m_input->setText(QStringLiteral("Микрофон")); + m_output->setText(QStringLiteral("Выход звука")); + m_input->setObjectName(QStringLiteral("radioInputButton")); + m_output->setObjectName(QStringLiteral("radioOutputButton")); + for (auto* button : {m_input, m_output}) { + button->setPopupMode(QToolButton::InstantPopup); + button->setMenu(new QMenu(button)); + row->addWidget(button); + connect(button->menu(), &QMenu::aboutToShow, this, [this, button]() { populateDevices(button, button == m_input); }); + } + m_status = new QLabel(this); + m_status->setObjectName(QStringLiteral("radioStatus")); + m_status->setMinimumWidth(0); + m_status->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); + row->addWidget(m_status, 1); + m_vad = new QPushButton(QStringLiteral("VAD"), this); + m_vad->setObjectName(QStringLiteral("radioVadButton")); + m_vad->setAccessibleName(QStringLiteral("Автоматическая передача по голосу")); + m_vad->setCheckable(true); + m_vad->setFixedSize(52, 34); + m_vad->setStyleSheet("QPushButton:checked { background: #3390EC; color: white; }"); + row->addWidget(m_vad); + m_ptt = new QPushButton(QStringLiteral("PTT"), this); + m_ptt->setObjectName(QStringLiteral("radioPttButton")); + m_ptt->setAccessibleName(QStringLiteral("Держите и говорите")); + m_ptt->setToolTip(QStringLiteral("Держите кнопку или глобальный хоткей для передачи")); + m_ptt->setFixedSize(64, 34); + m_ptt->setStyleSheet("QPushButton[transmitting=\"true\"] { background: #27ae60; color: white; }"); + row->addWidget(m_ptt); + auto* engine = RadioAudioEngine::instance(); + connect(m_ptt, &QPushButton::pressed, engine, &RadioAudioEngine::talkBegin); + connect(m_ptt, &QPushButton::released, engine, &RadioAudioEngine::talkEnd); + connect(m_vad, &QPushButton::clicked, this, [this](bool checked) { + RadioAudioEngine::instance()->applySettings({{QStringLiteral("radio_vad_enabled"), checked ? 1 : 0}}); + updateState(); + }); + connect(engine, &RadioAudioEngine::stateChanged, this, &RadioPanel::updateState); + connect(SoundManager::instance(), &SoundManager::devicesChanged, this, &RadioPanel::updateDevices); + updateState(); + updateDevices(); +} + +void RadioPanel::updateState() { + auto* engine = RadioAudioEngine::instance(); + QSignalBlocker blocker(m_vad); + m_vad->setChecked(engine->vadEnabled()); + m_vad->setEnabled(engine->isActive() && engine->settingsReady() && !engine->settingsBusy()); + m_ptt->setEnabled(engine->isActive() && engine->captureAvailable()); + bool transmitting = engine->isTransmitting(); + if (m_ptt->property("transmitting").toBool() != transmitting || !m_ptt->property("transmitting").isValid()) { + m_ptt->setProperty("transmitting", transmitting); + m_ptt->style()->unpolish(m_ptt); + m_ptt->style()->polish(m_ptt); + m_ptt->update(); + } + QString state; + if (!engine->settingsError().isEmpty()) state = QStringLiteral("Ошибка настройки"); + else if (engine->settingsBusy()) state = QStringLiteral("Применение настроек…"); + else if (!engine->captureAvailable()) state = QStringLiteral("Микрофон недоступен"); + else if (engine->vadEnabled() && !engine->vadAvailable()) state = QStringLiteral("VAD недоступен"); + else if (transmitting) state = QStringLiteral("Передача"); + else state = engine->vadEnabled() ? QStringLiteral("VAD слушает") : QStringLiteral("Приём"); + m_status->setText(state); + m_status->setToolTip(engine->settingsError().isEmpty() ? state : engine->settingsError()); + m_vad->setToolTip(engine->vadEnabled() ? QStringLiteral("Выключить автоматическую передачу по голосу") + : QStringLiteral("Включить автоматическую передачу по голосу")); +} + +/* Список перечитывается при открытии меню: подключённые устройства видны без перезапуска. */ +void RadioPanel::populateDevices(QToolButton* button, bool capture) { + auto* menu = button->menu(); + menu->clear(); + auto* group = new QActionGroup(menu); + group->setExclusive(true); + QString selected = capture ? SoundManager::instance()->captureDevice() : SoundManager::instance()->playbackDevice(); + auto add = [&](const QString& name, const QString& id) { + auto* action = menu->addAction(name); + action->setCheckable(true); + action->setChecked(id == selected); + group->addAction(action); + connect(action, &QAction::triggered, this, [this, id, capture]() { selectDevice(id, capture); }); + }; + add(QStringLiteral("По умолчанию (авто)"), QString()); + auto devices = capture ? SoundManager::enumerateCaptureDevices() : SoundManager::enumeratePlaybackDevices(); + for (const auto& device : devices) add(device.name, device.id); +} + +void RadioPanel::selectDevice(const QString& id, bool capture) { + auto* inst = gui_bridge_get_inst(); + if (!inst || !gui_bridge_is_uasync_ready()) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioPanel: cannot save device: core unavailable"); + m_status->setText(QStringLiteral("Ядро недоступно")); + return; + } + const char* key = capture ? "audio_input_id" : "audio_output_id"; + QByteArray value = id.toUtf8(); + auto* request = static_cast(u_calloc(1, sizeof(save_ui_state_arg))); + if (!request) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioPanel: device settings allocation failed"); + m_status->setText(QStringLiteral("Не удалось выбрать устройство")); + return; + } + size_t keyLen = strlen(key), valueLen = value.size(); + if (keyLen + valueLen + 2 > sizeof(request->data)) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioPanel: device ID too long bytes=%zu", valueLen); + u_free(request); + m_status->setText(QStringLiteral("Не удалось выбрать устройство")); + return; + } + request->inst = inst; + memcpy(request->data, key, keyLen + 1); + memcpy(request->data + keyLen + 1, value.constData(), valueLen + 1); + gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, request); + auto* manager = SoundManager::instance(); + DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioPanel: selected %s default=%d", capture ? "input" : "output", id.isEmpty()); + manager->setDevices(capture ? manager->playbackDevice() : id, capture ? id : manager->captureDevice()); +} + +void RadioPanel::updateDevices() { + auto* manager = SoundManager::instance(); + for (auto* button : {m_input, m_output}) { + bool capture = button == m_input; + QString id = capture ? manager->captureDevice() : manager->playbackDevice(); + QString name = QStringLiteral("По умолчанию (авто)"); + if (!id.isEmpty()) { + name = QStringLiteral("Выбранное устройство недоступно; используется доступное"); + auto devices = capture ? manager->enumerateCaptureDevices() : manager->enumeratePlaybackDevices(); + for (const auto& device : devices) if (device.id == id) { name = device.name; break; } + } + button->setToolTip(name); + button->setAccessibleName(button->text() + ": " + name); + } +} diff --git a/tools/chatgui/src/radiopanel.h b/tools/chatgui/src/radiopanel.h new file mode 100644 index 00000000..7f702cdb --- /dev/null +++ b/tools/chatgui/src/radiopanel.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +class QToolButton; +class QPushButton; +class QLabel; + +/* Верхняя панель активной рации: общие устройства SoundManager, VAD и удерживаемая PTT. + * Состояние передачи берётся из ядра; включение VAD хранится в конфигурации приложения. */ +class RadioPanel : public QWidget { + Q_OBJECT +public: + explicit RadioPanel(QWidget* parent = nullptr); +private: + void updateState(); + void updateDevices(); + void populateDevices(QToolButton* button, bool capture); + void selectDevice(const QString& id, bool capture); + QToolButton *m_input, *m_output; + QPushButton *m_vad, *m_ptt; + QLabel* m_status; +};