2 changed files with 181 additions and 0 deletions
@ -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); |
||||
} |
||||
} |
||||
@ -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…
Reference in new issue