Browse Source

Add desktop radio panel with device menus and VAD switch

master
evgeny 1 week ago
parent
commit
fa70093866
  1. 158
      tools/chatgui/src/radiopanel.cpp
  2. 23
      tools/chatgui/src/radiopanel.h

158
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 <QHBoxLayout>
#include <QToolButton>
#include <QPushButton>
#include <QLabel>
#include <QMenu>
#include <QActionGroup>
#include <QSignalBlocker>
#include <cstring>
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<save_ui_state_arg*>(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);
}
}

23
tools/chatgui/src/radiopanel.h

@ -0,0 +1,23 @@
#pragma once
#include <QWidget>
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;
};
Loading…
Cancel
Save