231 lines
11 KiB
C++
231 lines
11 KiB
C++
#include "radiosettingspage.h"
|
|
#include "ptt_hotkey_manager.h"
|
|
#include "radio_audio_engine.h"
|
|
#include "../db/db_manager.h"
|
|
#include "../../lib/debug_config.h"
|
|
|
|
#include <QApplication>
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QLabel>
|
|
#include <QPushButton>
|
|
#include <QHideEvent>
|
|
#include <QShowEvent>
|
|
#include <QFormLayout>
|
|
#include <QSpinBox>
|
|
#include <QCheckBox>
|
|
|
|
RadioSettingsPage::RadioSettingsPage(DbManager* db, QWidget* parent) : QWidget(parent), m_db(db) {
|
|
auto* layout = new QVBoxLayout(this);
|
|
auto* title = new QLabel("Рация (PTT)", this);
|
|
title->setStyleSheet("font-weight: bold; font-size: 13px;");
|
|
layout->addWidget(title);
|
|
m_audioSettings = new QWidget(this);
|
|
auto* form = new QFormLayout(m_audioSettings);
|
|
form->setContentsMargins(0, 0, 0, 12);
|
|
auto add = [&](const QString& label, int min, int max, const QString& suffix) {
|
|
auto* spin = new QSpinBox(m_audioSettings);
|
|
spin->setRange(min, max);
|
|
spin->setSuffix(suffix);
|
|
form->addRow(label, spin);
|
|
return spin;
|
|
};
|
|
m_threshold = add(QStringLiteral("Порог речи VAD:"), 10, 95, QStringLiteral(" %"));
|
|
m_threshold->setToolTip(QStringLiteral("Порог вероятности речи: меньше — чувствительнее. Это не порог громкости."));
|
|
m_hangover = add(QStringLiteral("Тишина до окончания передачи:"), 50, 2000, QStringLiteral(" мс"));
|
|
m_compressor = new QCheckBox(QStringLiteral("Компрессор передачи и VAD"), m_audioSettings);
|
|
form->addRow(m_compressor);
|
|
m_txGain = add(QStringLiteral("Max gain передачи:"), 1, 60, QStringLiteral(" dB"));
|
|
m_vadGain = add(QStringLiteral("Max gain VAD:"), 1, 60, QStringLiteral(" dB"));
|
|
auto* explanation = new QLabel(QStringLiteral("VAD включается кнопкой на панели рации. Буфер начала речи — 0,6 с. "
|
|
"Компрессор VAD имеет отдельный max gain; остальные параметры общие с компрессором передачи. "
|
|
"Скорость нарастания усиления задаётся в разделе Audio."), m_audioSettings);
|
|
explanation->setWordWrap(true);
|
|
form->addRow(explanation);
|
|
layout->addWidget(m_audioSettings);
|
|
connect(RadioAudioEngine::instance(), &RadioAudioEngine::stateChanged, this, [this]() {
|
|
auto* engine = RadioAudioEngine::instance();
|
|
m_audioSettings->setEnabled(engine->settingsReady() && !engine->settingsBusy());
|
|
if (!m_audioSettingsLoaded && engine->settingsReady()) loadAudioSettings();
|
|
});
|
|
loadAudioSettings();
|
|
if (PttHotkeyManager::usesPortal()) {
|
|
auto* hint = new QLabel("Глобальные хоткеи работают из любых приложений. "
|
|
"Назначьте обычную клавишу с Ctrl, Shift, Alt или Win в системном окне. "
|
|
"Изменения сохраняются системой сразу. Во время настройки передача с клавиатуры отключена.", this);
|
|
hint->setWordWrap(true);
|
|
layout->addWidget(hint);
|
|
auto* button = new QPushButton("Задать PTT хоткеи…", this);
|
|
layout->addWidget(button);
|
|
m_buttons << button;
|
|
for (int i = 0; i < 3; i++) {
|
|
auto* label = new QLabel(this);
|
|
label->setWordWrap(true); label->setTextFormat(Qt::PlainText);
|
|
layout->addWidget(label); m_labels << label;
|
|
}
|
|
m_portalStatus = new QLabel(this);
|
|
m_portalStatus->setWordWrap(true);
|
|
m_portalStatus->setTextFormat(Qt::PlainText);
|
|
m_portalStatus->setStyleSheet("color: gray; font-size: 11px;");
|
|
layout->addWidget(m_portalStatus);
|
|
connect(button, &QPushButton::clicked, this, [this]() { PttHotkeyManager::instance()->configurePortal(this); });
|
|
connect(PttHotkeyManager::instance(), &PttHotkeyManager::portalChanged, this, &RadioSettingsPage::updatePortalLabels);
|
|
updatePortalLabels();
|
|
layout->addStretch(1);
|
|
return;
|
|
}
|
|
auto* hint = new QLabel(
|
|
"Удерживайте сочетание для передачи из любого приложения — даже когда ChatGUI не в фокусе. "
|
|
"Рация канала должна быть включена. Для записи зажмите нужные клавиши вместе и отпустите их. "
|
|
"Левые и правые Ctrl, Shift, Alt, Win различаются. Fn доступна, только если клавиатура передаёт её ОС.", this);
|
|
hint->setWordWrap(true);
|
|
hint->setStyleSheet("color: gray; font-size: 11px;");
|
|
layout->addWidget(hint);
|
|
if (!PttHotkeyManager::isSupported()) {
|
|
auto* warn = new QLabel("Глобальные хоткеи доступны на Windows и Linux/X11. На этой платформе запись недоступна.", this);
|
|
warn->setWordWrap(true);
|
|
warn->setStyleSheet("color: #e74c3c; font-size: 12px;");
|
|
layout->addWidget(warn);
|
|
}
|
|
for (int i = 0; i < 3; i++) {
|
|
auto* row = new QHBoxLayout();
|
|
auto* button = new QPushButton(QString("Задать PTT хоткей %1").arg(i + 1), this);
|
|
button->setEnabled(PttHotkeyManager::isSupported());
|
|
auto* clear = new QPushButton("Очистить", this);
|
|
row->addWidget(button); row->addWidget(clear);
|
|
layout->addLayout(row);
|
|
auto* label = new QLabel(this);
|
|
label->setWordWrap(true);
|
|
label->setTextFormat(Qt::PlainText);
|
|
label->setStyleSheet("font-size: 13px; padding-bottom: 12px;");
|
|
layout->addWidget(label);
|
|
m_buttons << button; m_labels << label; m_chords << PttKeyChord();
|
|
connect(button, &QPushButton::clicked, this, [this, i]() {
|
|
if (m_captureIndex == i) finishCapture(false);
|
|
else startCapture(i);
|
|
});
|
|
connect(clear, &QPushButton::clicked, this, [this, i]() {
|
|
finishCapture(false);
|
|
m_chords[i].clear(); updateLabel(i);
|
|
});
|
|
}
|
|
connect(PttHotkeyManager::instance(), &PttHotkeyManager::keysChanged, this, [this](const PttKeyChord& held) {
|
|
if (m_captureIndex < 0) return;
|
|
if (m_capture.observe(held)) finishCapture(true);
|
|
else if (!held.isEmpty())
|
|
m_labels[m_captureIndex]->setText(PttHotkeyManager::instance()->describe(m_capture.candidate));
|
|
});
|
|
layout->addStretch(1);
|
|
loadFromDb();
|
|
}
|
|
|
|
RadioSettingsPage::~RadioSettingsPage() {
|
|
finishCapture(false);
|
|
if (m_portalStatus && isVisible()) PttHotkeyManager::instance()->inhibitPortal(false);
|
|
}
|
|
|
|
void RadioSettingsPage::startCapture(int index) {
|
|
finishCapture(false);
|
|
if (!PttHotkeyManager::instance()->beginCapture()) {
|
|
m_labels[index]->setText("Не удалось начать запись клавиш. Подробности в логе radio.");
|
|
return;
|
|
}
|
|
m_captureIndex = index;
|
|
m_capture.candidate.clear();
|
|
m_buttons[index]->setText("Отменить запись");
|
|
m_labels[index]->setText("Нажмите одну или несколько клавиш вместе…");
|
|
qApp->installEventFilter(this);
|
|
}
|
|
|
|
void RadioSettingsPage::finishCapture(bool save) {
|
|
if (m_captureIndex < 0) return;
|
|
int index = m_captureIndex;
|
|
if (save) {
|
|
m_chords[index] = m_capture.candidate;
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "PTT: captured chord=%s", qPrintable(PttHotkeyManager::instance()->describe(m_capture.candidate)));
|
|
}
|
|
m_captureIndex = -1;
|
|
qApp->removeEventFilter(this);
|
|
PttHotkeyManager::instance()->endCapture();
|
|
m_buttons[index]->setText(QString("Задать PTT хоткей %1").arg(index + 1));
|
|
updateLabel(index);
|
|
}
|
|
|
|
bool RadioSettingsPage::eventFilter(QObject* object, QEvent* event) {
|
|
if (m_captureIndex >= 0) {
|
|
if (event->type() == QEvent::ShortcutOverride) { event->accept(); return true; }
|
|
if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) return true;
|
|
}
|
|
return QWidget::eventFilter(object, event);
|
|
}
|
|
|
|
void RadioSettingsPage::showEvent(QShowEvent* event) {
|
|
if (m_portalStatus) PttHotkeyManager::instance()->inhibitPortal(true);
|
|
QWidget::showEvent(event);
|
|
}
|
|
|
|
void RadioSettingsPage::hideEvent(QHideEvent* event) {
|
|
if (m_portalStatus) PttHotkeyManager::instance()->inhibitPortal(false);
|
|
finishCapture(false);
|
|
QWidget::hideEvent(event);
|
|
}
|
|
|
|
void RadioSettingsPage::updateLabel(int index) {
|
|
m_labels[index]->setText(PttHotkeyManager::instance()->describe(m_chords[index]));
|
|
}
|
|
|
|
void RadioSettingsPage::loadFromDb() {
|
|
loadAudioSettings();
|
|
if (m_portalStatus) { updatePortalLabels(); return; }
|
|
finishCapture(false);
|
|
QStringList values;
|
|
if (m_db && m_db->isOpen()) values = m_db->getUiState("radio_ptt_hotkeys").split('|', Qt::KeepEmptyParts);
|
|
for (int i = 0; i < m_chords.size(); i++) {
|
|
m_chords[i] = i < values.size() ? pttDecodeChord(values[i]) : PttKeyChord();
|
|
updateLabel(i);
|
|
}
|
|
}
|
|
|
|
QStringList RadioSettingsPage::hotkeys() const {
|
|
QStringList values;
|
|
// Пустые позиции тоже сохраняем, чтобы хоткей №3 не переезжал в №1 после открытия.
|
|
for (const auto& chord : m_chords) values << pttEncodeChord(chord);
|
|
return values;
|
|
}
|
|
|
|
bool RadioSettingsPage::apply() {
|
|
if (!RadioAudioEngine::instance()->applySettings({
|
|
{QStringLiteral("radio_vad_threshold"), m_threshold->value()},
|
|
{QStringLiteral("radio_vad_hangover_ms"), m_hangover->value()},
|
|
{QStringLiteral("radio_compressor_enabled"), m_compressor->isChecked() ? 1 : 0},
|
|
{QStringLiteral("radio_compressor_max_gain_db"), m_txGain->value()},
|
|
{QStringLiteral("radio_vad_compressor_max_gain_db"), m_vadGain->value()}})) return false;
|
|
if (m_portalStatus) return true;
|
|
finishCapture(false);
|
|
PttHotkeyManager::instance()->setHotkeys(hotkeys());
|
|
return true;
|
|
}
|
|
|
|
void RadioSettingsPage::loadAudioSettings() {
|
|
auto* engine = RadioAudioEngine::instance();
|
|
m_audioSettings->setEnabled(engine->settingsReady() && !engine->settingsBusy());
|
|
if (!engine->settingsReady()) return;
|
|
m_threshold->setValue(engine->setting(QStringLiteral("radio_vad_threshold")));
|
|
m_hangover->setValue(engine->setting(QStringLiteral("radio_vad_hangover_ms")));
|
|
m_compressor->setChecked(engine->setting(QStringLiteral("radio_compressor_enabled")) != 0);
|
|
m_txGain->setValue(engine->setting(QStringLiteral("radio_compressor_max_gain_db")));
|
|
m_vadGain->setValue(engine->setting(QStringLiteral("radio_vad_compressor_max_gain_db")));
|
|
m_audioSettingsLoaded = true;
|
|
}
|
|
|
|
void RadioSettingsPage::updatePortalLabels() {
|
|
auto* manager = PttHotkeyManager::instance();
|
|
const auto descriptions = manager->portalDescriptions();
|
|
for (int i = 0; i < m_labels.size(); i++) {
|
|
QString text = descriptions.value(i);
|
|
m_labels[i]->setText(QString("PTT хоткей %1: %2").arg(i + 1).arg(text.isEmpty() ? "Не задано" : text));
|
|
}
|
|
m_portalStatus->setText(manager->portalStatus());
|
|
m_buttons[0]->setEnabled(!manager->portalBusy());
|
|
}
|