Browse Source

Show speaking waves for locally muted radio members

master
evgeny 4 days ago
parent
commit
d5174262d8
  1. 2
      doc/tasks.md
  2. 7
      tools/chatgui/src/accountdelegate.cpp
  3. 15
      tools/chatgui/tests/test_radio_mute_bridge.cpp

2
doc/tasks.md

@ -4,7 +4,7 @@
## Текущая задача ## Текущая задача
[+] **Динамик участника показывает речь в рации ChatGUI** — 07.10.2026: [+] **Динамик участника показывает речь в рации ChatGUI** — 07.10.2026:
без волн — молчит, с волнами — говорит, красное перечёркивание — локально заглушён (приоритет над речью). без волн — молчит, с волнами — говорит, красное перечёркивание — локально заглушён; волны видны и при mute.
START/STOP обновляют строки через Qt bridge; собственная передача учитывает PTT, хоткеи и VAD. START/STOP обновляют строки через Qt bridge; собственная передача учитывает PTT, хоткеи и VAD.
Обновления списка/RTT сохраняют индикацию; смена канала, выключение рации и stop очищают её. Обновления списка/RTT сохраняют индикацию; смена канала, выключение рации и stop очищают её.
Исправлен сдвиг индексов строк при удалении участника. Добавлены переводы подсказок и логи radio. Исправлен сдвиг индексов строк при удалении участника. Добавлены переводы подсказок и логи radio.

7
tools/chatgui/src/accountdelegate.cpp

@ -23,7 +23,7 @@ static QRect radioMuteButtonRect(const QRect& row) {
return QRect(row.right() - 63, row.center().y() - 14, 28, 28); return QRect(row.right() - 63, row.center().y() - 14, 28, 28);
} }
// Волны показывают передачу, красное перечёркивание имеет приоритет при локальном mute. // Волны показывают передачу независимо от локального mute; перечёркивание сохраняется при речи.
static void drawRadioMuteButton(QPainter* painter, const QRect& button, bool muted, bool speaking) { static void drawRadioMuteButton(QPainter* painter, const QRect& button, bool muted, bool speaking) {
painter->save(); painter->save();
painter->setRenderHint(QPainter::Antialiasing); painter->setRenderHint(QPainter::Antialiasing);
@ -35,7 +35,7 @@ static void drawRadioMuteButton(QPainter* painter, const QRect& button, bool mut
painter->fillPath(speaker, color); painter->fillPath(speaker, color);
painter->setPen(QPen(color, 1.8, Qt::SolidLine, Qt::RoundCap)); painter->setBrush(Qt::NoBrush); painter->setPen(QPen(color, 1.8, Qt::SolidLine, Qt::RoundCap)); painter->setBrush(Qt::NoBrush);
if (muted) painter->drawLine(QPointF(-10, 10), QPointF(10, -10)); if (muted) painter->drawLine(QPointF(-10, 10), QPointF(10, -10));
else if (speaking) { if (speaking) {
painter->drawArc(QRectF(-3, -5, 10, 10), -60 * 16, 120 * 16); painter->drawArc(QRectF(-3, -5, 10, 10), -60 * 16, 120 * 16);
painter->drawArc(QRectF(-6, -8, 16, 16), -60 * 16, 120 * 16); painter->drawArc(QRectF(-6, -8, 16, 16), -60 * 16, 120 * 16);
} }
@ -176,7 +176,8 @@ bool AccountDelegate::helpEvent(QHelpEvent* event, QAbstractItemView* view, cons
if (event && radioMuteButtonRect(option.rect).contains(event->pos())) { if (event && radioMuteButtonRect(option.rect).contains(event->pos())) {
const bool muted = index.data(Qt::UserRole + 3).toInt() & CHAT_MEMBER_FLAG_RADIO_MUTED; const bool muted = index.data(Qt::UserRole + 3).toInt() & CHAT_MEMBER_FLAG_RADIO_MUTED;
const bool speaking = index.data(MemberListModel::RadioSpeakingRole).toBool(); const bool speaking = index.data(MemberListModel::RadioSpeakingRole).toBool();
QString text = QCoreApplication::translate("ChatGui", muted ? "Radio: muted" : speaking ? "Radio: speaking" : "Radio: silent"); QString text = QCoreApplication::translate("ChatGui", speaking ? "Radio: speaking" : "Radio: silent");
if (muted) text += " · " + QCoreApplication::translate("ChatGui", "Radio: muted");
if (!index.data(Qt::UserRole + 2).toBool()) text += '\n' + QCoreApplication::translate("ChatGui", if (!index.data(Qt::UserRole + 2).toBool()) text += '\n' + QCoreApplication::translate("ChatGui",
muted ? "Listen to this member in radio" : "Mute this member in radio"); muted ? "Listen to this member in radio" : "Mute this member in radio");
QToolTip::showText(event->globalPos(), text, view); QToolTip::showText(event->globalPos(), text, view);

15
tools/chatgui/tests/test_radio_mute_bridge.cpp

@ -151,11 +151,18 @@ private slots:
QTRY_COMPARE_WITH_TIMEOUT(probe.results.size(), 1, 3000); QVERIFY(probe.results[0]); QTRY_COMPARE_WITH_TIMEOUT(probe.results.size(), 1, 3000); QVERIFY(probe.results[0]);
QCOMPARE(probe.updates, 1); QVERIFY(source.data(Qt::UserRole + 3).toInt() & CHAT_MEMBER_FLAG_RADIO_MUTED); QCOMPARE(probe.updates, 1); QVERIFY(source.data(Qt::UserRole + 3).toInt() & CHAT_MEMBER_FLAG_RADIO_MUTED);
QVERIFY(iconHasColor(QColor("#EF5350"))); QVERIFY(!iconHasColor(QColor("#4CAF50"))); QVERIFY(iconHasColor(QColor("#EF5350"))); QVERIFY(!iconHasColor(QColor("#4CAF50")));
const QImage muted = iconImage(); const QImage mutedSpeaking = iconImage();
QVERIFY(muted.pixelColor(22, 6).red() > 180); // Красное перечёркивание правее корпуса динамика. auto redPixel = [](const QImage& image, int x, int y) {
const QColor color = image.pixelColor(x, y);
return color.red() > color.green() + 50;
};
QVERIFY(redPixel(mutedSpeaking, 21, 5)); // Красное перечёркивание правее корпуса динамика.
QVERIFY(redPixel(mutedSpeaking, 23, 13)); // Волна видна и при локальном mute.
post_talk(group, 42, false); post_talk(group, 42, false);
QTRY_VERIFY(!source.data(MemberListModel::RadioSpeakingRole).toBool()); QTRY_VERIFY(!source.data(MemberListModel::RadioSpeakingRole).toBool());
QCOMPARE(iconImage(), muted); // Mute имеет приоритет над состоянием речи. const QImage mutedSilent = iconImage();
QVERIFY(mutedSilent != mutedSpeaking);
QVERIFY(redPixel(mutedSilent, 21, 5)); QVERIFY(!redPixel(mutedSilent, 23, 13));
QVERIFY(other.data(MemberListModel::RadioSpeakingRole).toBool()); QVERIFY(other.data(MemberListModel::RadioSpeakingRole).toBool());
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, button); QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, button);
QCOMPARE(accepted, 0); QTRY_COMPARE_WITH_TIMEOUT(probe.results.size(), 2, 3000); QVERIFY(probe.results[1]); QCOMPARE(accepted, 0); QTRY_COMPARE_WITH_TIMEOUT(probe.results.size(), 2, 3000); QVERIFY(probe.results[1]);
@ -202,7 +209,7 @@ private slots:
model.setChannel(QString(), false); QCOMPARE(model.rowCount(), 0); model.setChannel(QString(), false); QCOMPARE(model.rowCount(), 0);
model.setChannel(probe.channel, false); QTRY_COMPARE_WITH_TIMEOUT(model.rowCount(), 3, 3000); model.setChannel(probe.channel, false); QTRY_COMPARE_WITH_TIMEOUT(model.rowCount(), 3, 3000);
QVERIFY(!memberIndex(42).data(MemberListModel::RadioSpeakingRole).toBool()); QVERIFY(!memberIndex(42).data(MemberListModel::RadioSpeakingRole).toBool());
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "radio members test: shapes, mute priority, multiple speakers, refresh, self and reset PASS"); DEBUG_INFO(DEBUG_CATEGORY_RADIO, "radio members test: shapes, speech while muted, multiple speakers, refresh, self and reset PASS");
node.stop(); QVERIFY(!gui_bridge_is_uasync_ready()); node.stop(); QVERIFY(!gui_bridge_is_uasync_ready());
QCOMPARE(gui_bridge_toggle_member_radio_mute(probe.channel.toUtf8().constData(), 42), -1); QCOMPARE(gui_bridge_toggle_member_radio_mute(probe.channel.toUtf8().constData(), 42), -1);
gui_bridge_set_member_updated_cb(nullptr); gui_bridge_set_radio_mute_result_cb(nullptr); current = nullptr; gui_bridge_set_member_updated_cb(nullptr); gui_bridge_set_radio_mute_result_cb(nullptr); current = nullptr;

Loading…
Cancel
Save