#include "radiosettingspage.h" #include "ptt_hotkey_manager.h" #include "radio_audio_engine.h" #include "../db/db_manager.h" #include "../../lib/debug_config.h" #include #include #include #include #include #include #include #include #include #include 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()); }