You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
399 lines
21 KiB
399 lines
21 KiB
#include <QCoreApplication> |
|
#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> |
|
#include <QButtonGroup> |
|
#include <QPainter> |
|
#include <algorithm> |
|
#include <QKeyEvent> |
|
#ifdef Q_OS_WIN |
|
#include <qt_windows.h> |
|
#endif |
|
|
|
/* Шкала показывает именно RMS детектора; отметки соответствуют редактируемым порогам. */ |
|
class RadioLevelMeter : public QWidget { |
|
public: |
|
explicit RadioLevelMeter(QWidget* parent) : QWidget(parent) { setMinimumSize(200, 34); } |
|
float level = -120; |
|
int on = -35, off = -45; |
|
bool valid = false, vad = false; |
|
protected: |
|
void paintEvent(QPaintEvent*) override { |
|
QPainter painter(this); |
|
QRect bar(1, 1, width() - 2, 16); |
|
painter.fillRect(bar, palette().color(QPalette::Mid)); |
|
auto x = [&](float value) { return bar.left() + int(std::clamp(vad ? value / 100 : (value + 90) / 90, 0.0f, 1.0f) * bar.width()); }; |
|
if (valid) painter.fillRect(QRect(bar.left(), bar.top(), x(level) - bar.left(), bar.height()), QColor("#27ae60")); |
|
painter.setPen(palette().color(QPalette::Text)); |
|
painter.drawText(QRect(0, 18, width(), 16), Qt::AlignLeft, vad ? QStringLiteral("0%") : QStringLiteral("−90 dBFS")); |
|
painter.drawText(QRect(0, 18, width(), 16), Qt::AlignRight, vad ? QStringLiteral("100%") : QStringLiteral("0")); |
|
for (int threshold : {off, on}) { |
|
if (vad && threshold == off) continue; |
|
painter.setPen(QPen(threshold == on ? QColor("#e67e22") : QColor("#3390ec"), 2)); |
|
painter.drawLine(x(threshold), 0, x(threshold), 18); |
|
} |
|
} |
|
}; |
|
|
|
RadioActivationSettings::RadioActivationSettings(bool compact, QWidget* parent) : QWidget(parent), m_compact(compact) { |
|
auto* form = new QFormLayout(this); |
|
form->setContentsMargins(0, 0, 0, 8); |
|
form->setRowWrapPolicy(QFormLayout::WrapLongRows); |
|
form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); |
|
auto add = [&](const QString& label, const char* name, int min, int max, const QString& suffix) { |
|
auto* spin = new QSpinBox(this); |
|
spin->setObjectName(QLatin1String(name)); spin->setRange(min, max); spin->setSuffix(suffix); |
|
auto* text = new QLabel(label, this); text->setWordWrap(true); |
|
form->addRow(text, spin); |
|
connect(spin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RadioActivationSettings::updateControls); |
|
return spin; |
|
}; |
|
auto label = [&](const char* name) { |
|
auto* text = new QLabel(this); text->setObjectName(QLatin1String(name)); text->setWordWrap(true); |
|
form->addRow(text); return text; |
|
}; |
|
auto* modes = new QWidget(this); |
|
auto* row = new QHBoxLayout(modes); row->setContentsMargins(0, 0, 0, 0); row->setSpacing(0); |
|
m_mode = new QButtonGroup(this); |
|
for (int mode : {1, 2}) { |
|
auto* button = new QPushButton(mode == 1 ? QStringLiteral("VAD") : QCoreApplication::translate("ChatGui", "By signal level"), modes); |
|
button->setObjectName(mode == 1 ? QStringLiteral("radioModeVad") : QStringLiteral("radioModeLevel")); |
|
button->setCheckable(true); button->setMinimumHeight(36); |
|
button->setStyleSheet("QPushButton:checked { background: #3390EC; color: white; }"); |
|
m_mode->addButton(button, mode); row->addWidget(button, 1); |
|
connect(button, &QPushButton::clicked, this, [this, mode]() { |
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "Radio settings: select mode=%d compact=%d (pending Apply)", mode, m_compact); |
|
updateControls(); |
|
}); |
|
} |
|
m_mode->button(1)->setChecked(true); |
|
form->addRow(QCoreApplication::translate("ChatGui", "Automatic transmission:"), modes); |
|
m_levelText = label("radioLevelText"); |
|
m_levelMeter = new RadioLevelMeter(this); m_levelMeter->setObjectName(QStringLiteral("radioLevelMeter")); |
|
form->addRow(m_levelMeter); |
|
m_levelOn = add(QCoreApplication::translate("ChatGui", "Start threshold (orange):"), "radioLevelOn", -90, 0, QStringLiteral(" dBFS")); |
|
m_levelOff = add(QCoreApplication::translate("ChatGui", "Stop threshold (blue):"), "radioLevelOff", -90, 0, QStringLiteral(" dBFS")); |
|
m_vadText = label("radioVadText"); |
|
m_vadMeter = new RadioLevelMeter(this); m_vadMeter->vad = true; m_vadMeter->setObjectName(QStringLiteral("radioVadMeter")); |
|
form->addRow(m_vadMeter); |
|
m_threshold = add(QCoreApplication::translate("ChatGui", "VAD speech threshold:"), "radioVadThreshold", 10, 95, QStringLiteral(" %")); |
|
m_threshold->setToolTip(QCoreApplication::translate("ChatGui", |
|
"Speech probability threshold: lower is more sensitive. This is not a volume threshold.")); |
|
m_gainText = label("radioVadGainText"); |
|
m_vadGain = add(QCoreApplication::translate("ChatGui", "VAD max gain:"), "radioVadGain", 1, 60, QCoreApplication::translate("ChatGui", " dB")); |
|
m_hangover = add(QCoreApplication::translate("ChatGui", "Silence before ending transmission:"), "radioHangover", 50, 2000, |
|
QCoreApplication::translate("ChatGui", " ms")); |
|
if (!compact) { |
|
m_compressor = new QCheckBox(QCoreApplication::translate("ChatGui", "Transmission and VAD compressor"), this); |
|
form->addRow(m_compressor); |
|
connect(m_compressor, &QCheckBox::toggled, this, &RadioActivationSettings::updateControls); |
|
m_txGain = add(QCoreApplication::translate("ChatGui", "Transmission max gain:"), "radioTxGain", 1, 60, |
|
QCoreApplication::translate("ChatGui", " dB")); |
|
label("radioActivationHint")->setText(QCoreApplication::translate("ChatGui", |
|
"Enable automatic transmission using the radio panel button. Both modes include 0.6 s of prebuffer. " |
|
"Signal level is measured after echo cancellation, before compression. Start requires 40 ms above the start threshold; " |
|
"transmission ends after the configured silence below the stop threshold. The VAD compressor has a separate maximum gain.")); |
|
} |
|
m_error = label("radioActivationError"); m_error->setStyleSheet(QStringLiteral("color: #e74c3c;")); |
|
auto* engine = RadioAudioEngine::instance(); |
|
connect(engine, &RadioAudioEngine::stateChanged, this, [this, engine]() { |
|
setEnabled(engine->settingsReady() && !engine->settingsBusy()); |
|
if (!m_loaded && engine->settingsReady()) loadSettings(); |
|
else updateControls(); |
|
}); |
|
connect(engine, &RadioAudioEngine::inputLevelChanged, this, &RadioActivationSettings::updateMeters); |
|
loadSettings(); |
|
} |
|
|
|
QMap<QString, int> RadioActivationSettings::values() const { |
|
QMap<QString, int> result{{"radio_auto_mode", m_mode->checkedId()}, {"radio_level_on_dbfs", m_levelOn->value()}, |
|
{"radio_level_off_dbfs", m_levelOff->value()}, {"radio_vad_threshold", m_threshold->value()}, |
|
{"radio_auto_hangover_ms", m_hangover->value()}, {"radio_vad_compressor_max_gain_db", m_vadGain->value()}}; |
|
if (!m_compact) { |
|
result.insert(QStringLiteral("radio_compressor_enabled"), m_compressor->isChecked() ? 1 : 0); |
|
result.insert(QStringLiteral("radio_compressor_max_gain_db"), m_txGain->value()); |
|
} |
|
return result; |
|
} |
|
|
|
bool RadioActivationSettings::canApply() const { |
|
auto* engine = RadioAudioEngine::instance(); |
|
if (!m_loaded || !engine->settingsReady() || engine->settingsBusy() || m_levelOff->value() >= m_levelOn->value()) return false; |
|
const auto draft = values(); |
|
for (auto it = draft.cbegin(); it != draft.cend(); ++it) if (engine->setting(it.key()) != it.value()) return true; |
|
return false; |
|
} |
|
|
|
bool RadioActivationSettings::apply() { |
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "Radio settings: apply compact=%d mode=%d on=%d off=%d vad=%d gain=%d hangover=%d", |
|
m_compact, m_mode->checkedId(), m_levelOn->value(), m_levelOff->value(), m_threshold->value(), |
|
m_vadGain->value(), m_hangover->value()); |
|
return RadioAudioEngine::instance()->applySettings(values()); |
|
} |
|
|
|
void RadioActivationSettings::loadSettings() { |
|
auto* engine = RadioAudioEngine::instance(); |
|
setEnabled(engine->settingsReady() && !engine->settingsBusy()); |
|
if (!engine->settingsReady()) return; |
|
m_loaded = false; |
|
m_mode->button(engine->autoMode())->setChecked(true); |
|
m_levelOn->setValue(engine->setting(QStringLiteral("radio_level_on_dbfs"))); |
|
m_levelOff->setValue(engine->setting(QStringLiteral("radio_level_off_dbfs"))); |
|
m_threshold->setValue(engine->setting(QStringLiteral("radio_vad_threshold"))); |
|
m_hangover->setValue(engine->setting(QStringLiteral("radio_auto_hangover_ms"))); |
|
m_vadGain->setValue(engine->setting(QStringLiteral("radio_vad_compressor_max_gain_db"))); |
|
if (!m_compact) { |
|
m_compressor->setChecked(engine->setting(QStringLiteral("radio_compressor_enabled")) != 0); |
|
m_txGain->setValue(engine->setting(QStringLiteral("radio_compressor_max_gain_db"))); |
|
} |
|
m_loaded = true; |
|
updateControls(); |
|
} |
|
|
|
void RadioActivationSettings::updateControls() { |
|
if (!m_loaded) return; |
|
QString error = m_levelOff->value() >= m_levelOn->value() |
|
? QCoreApplication::translate("ChatGui", "The stop threshold must be lower than the start threshold") |
|
: RadioAudioEngine::instance()->settingsError(); |
|
m_error->setText(error); m_error->setVisible(!error.isEmpty()); |
|
m_levelMeter->on = m_levelOn->value(); m_levelMeter->off = m_levelOff->value(); |
|
m_vadMeter->on = m_threshold->value(); |
|
updateMeters(); |
|
emit changed(); |
|
} |
|
|
|
void RadioActivationSettings::updateMeters() { |
|
auto* engine = RadioAudioEngine::instance(); |
|
m_levelMeter->valid = engine->inputLevelValid(); m_levelMeter->level = engine->inputLevel(); m_levelMeter->update(); |
|
QString text; |
|
if (!engine->isActive()) text = QCoreApplication::translate("ChatGui", "Enable radio to measure the microphone level"); |
|
else if (!engine->inputLevelValid()) text = QCoreApplication::translate("ChatGui", "Microphone level unavailable"); |
|
else text = QCoreApplication::translate("ChatGui", "Current level: %1 dBFS").arg(engine->inputLevel(), 0, 'f', 1); |
|
m_levelText->setText(text); m_levelMeter->setAccessibleName(text); |
|
m_vadMeter->valid = engine->vadValid(); m_vadMeter->level = engine->vadProbability() * 100; m_vadMeter->update(); |
|
if (engine->vadValid()) text = QCoreApplication::translate("ChatGui", "VAD speech probability: %1%").arg(engine->vadProbability() * 100, 0, 'f', 1); |
|
else if (!engine->autoEnabled() || engine->autoMode() != 1) |
|
text = QCoreApplication::translate("ChatGui", "Enable automatic transmission in VAD mode to see speech probability"); |
|
else if (engine->autoBlocked()) text = QCoreApplication::translate("ChatGui", "VAD measurement paused: channel busy"); |
|
else text = QCoreApplication::translate("ChatGui", "VAD speech probability unavailable"); |
|
m_vadText->setText(text); m_vadMeter->setAccessibleName(text); |
|
if (!engine->vadValid()) text = QCoreApplication::translate("ChatGui", "Current VAD compressor gain: —"); |
|
else if (!engine->vadCompressorEnabled()) text = QCoreApplication::translate("ChatGui", "VAD compressor off (0 dB)"); |
|
else text = QCoreApplication::translate("ChatGui", "Current VAD compressor gain: %1 dB") |
|
.arg((engine->vadGain() >= 0 ? QStringLiteral("+") : QString()) + QString::number(engine->vadGain(), 'f', 1)); |
|
m_gainText->setText(text); |
|
} |
|
|
|
RadioSettingsPage::RadioSettingsPage(DbManager* db, QWidget* parent) : QWidget(parent), m_db(db) { |
|
auto* layout = new QVBoxLayout(this); |
|
auto* title = new QLabel(QCoreApplication::translate("ChatGui", "Radio (PTT)"), this); |
|
title->setStyleSheet("font-weight: bold; font-size: 13px;"); |
|
layout->addWidget(title); |
|
m_activation = new RadioActivationSettings(false, this); |
|
layout->addWidget(m_activation); |
|
if (PttHotkeyManager::usesPortal()) { |
|
auto* hint = new QLabel(QCoreApplication::translate("ChatGui", |
|
"Global shortcuts work from any application. Assign a regular key with Ctrl, Shift, Alt or " |
|
"Win in the system dialog. Changes are saved by the system immediately. Keyboard " |
|
"transmission is disabled during setup."), this); |
|
hint->setWordWrap(true); |
|
layout->addWidget(hint); |
|
auto* button = new QPushButton(QCoreApplication::translate("ChatGui", "Set PTT shortcuts…"), 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( |
|
QCoreApplication::translate("ChatGui", |
|
"Hold a shortcut to transmit from any application, even when ChatGUI is not focused. " |
|
"Channel radio must be enabled. To record a shortcut, hold the desired keys together and " |
|
"release them. Left and right Ctrl, Shift, Alt and Win are distinct. Fn is available only " |
|
"if the keyboard reports it to the OS."), this); |
|
hint->setWordWrap(true); |
|
hint->setStyleSheet("color: gray; font-size: 11px;"); |
|
layout->addWidget(hint); |
|
if (!PttHotkeyManager::isSupported()) { |
|
auto* warn = new QLabel(QCoreApplication::translate("ChatGui", |
|
"Global shortcuts are available on Windows and Linux/X11. Shortcut recording is " |
|
"unavailable on this platform."), 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(QCoreApplication::translate("ChatGui", "Set PTT shortcut %1").arg(i + 1), this); |
|
button->setEnabled(PttHotkeyManager::isSupported()); |
|
auto* clear = new QPushButton(QCoreApplication::translate("ChatGui", "Clear"), 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); |
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "PTT: recording requested slot=%d", index + 1); |
|
if (!PttHotkeyManager::instance()->beginCapture()) { |
|
m_labels[index]->setText(QCoreApplication::translate("ChatGui", "Failed to start key recording. See the radio log for details.")); |
|
return; |
|
} |
|
m_captureIndex = index; |
|
m_capture.candidate.clear(); |
|
m_buttons[index]->setText(QCoreApplication::translate("ChatGui", "Cancel recording")); |
|
m_labels[index]->setText(QCoreApplication::translate("ChatGui", "Press one or more keys together…")); |
|
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 slot=%d chord=[%s] codes=[%s]", index + 1, |
|
qPrintable(PttHotkeyManager::instance()->describe(m_capture.candidate)), qPrintable(pttEncodeChord(m_capture.candidate))); |
|
} else { |
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "PTT: recording cancelled slot=%d candidate=[%s]", index + 1, |
|
qPrintable(pttEncodeChord(m_capture.candidate))); |
|
} |
|
m_captureIndex = -1; |
|
qApp->removeEventFilter(this); |
|
PttHotkeyManager::instance()->endCapture(); |
|
m_buttons[index]->setText(QCoreApplication::translate("ChatGui", "Set PTT shortcut %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) { |
|
auto* key = static_cast<QKeyEvent*>(event); |
|
// Только ручная запись: сравниваем доставку событий окну с независимым опросом ОС. |
|
if (!key->isAutoRepeat()) { |
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "PTT: capture window event=%s slot=%d qt=0x%x native_vk=%u scan=%u modifiers=0x%x", |
|
event->type() == QEvent::KeyPress ? "press" : "release", m_captureIndex + 1, |
|
unsigned(key->key()), unsigned(key->nativeVirtualKey()), unsigned(key->nativeScanCode()), |
|
unsigned(key->nativeModifiers())); |
|
#ifdef Q_OS_WIN |
|
int vk = int(key->nativeVirtualKey()); |
|
if (vk > 0 && vk < 256) { |
|
unsigned asyncState = static_cast<unsigned short>(GetAsyncKeyState(vk)); |
|
unsigned queueState = static_cast<unsigned short>(GetKeyState(vk)); |
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "PTT: capture Windows vk=%d async=0x%04x queue=0x%04x remote_session=%d", |
|
vk, asyncState, queueState, GetSystemMetrics(SM_REMOTESESSION)); |
|
} |
|
#endif |
|
} |
|
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() { |
|
m_activation->loadSettings(); |
|
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 (!m_activation->apply()) return false; |
|
if (m_portalStatus) return true; |
|
finishCapture(false); |
|
PttHotkeyManager::instance()->setHotkeys(hotkeys()); |
|
return 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(QCoreApplication::translate("ChatGui", |
|
"PTT shortcut %1: %2").arg(i + 1).arg(text.isEmpty() ? QCoreApplication::translate("ChatGui", |
|
"Not set") : text)); |
|
} |
|
m_portalStatus->setText(manager->portalStatus()); |
|
m_buttons[0]->setEnabled(!manager->portalBusy()); |
|
}
|
|
|