diff --git a/tools/chatgui/src/capture_controls.h b/tools/chatgui/src/capture_controls.h index 13ef08e5..8c49db9d 100644 --- a/tools/chatgui/src/capture_controls.h +++ b/tools/chatgui/src/capture_controls.h @@ -135,11 +135,13 @@ inline QIcon controlIcon(const QPalette& palette, const QString& shape, const QS painter.drawRoundedRect(QRectF(15, 3, 10, 19), 5, 5); painter.drawArc(QRectF(10, 12, 20, 16), 180 * 16, 180 * 16); painter.drawLine(20, 28, 20, 33); painter.drawLine(14, 33, 26, 33); painter.restore(); labelRect = QRectF(1, 23, 38, 13); labelSize = 9; - } else if (shape == "sound" || shape == "silent") { + } else if (shape == "sound" || shape == "silent" || shape == "output") { + if (shape == "output") { painter.save(); painter.translate(7, 0); painter.scale(0.65, 0.65); } QPolygon speaker; speaker << QPoint(6, 14) << QPoint(13, 14) << QPoint(22, 7) << QPoint(22, 29) << QPoint(13, 22) << QPoint(6, 22); painter.drawPolygon(speaker); - if (shape == "sound") painter.drawArc(QRectF(18, 6, 17, 24), -70 * 16, 140 * 16); + if (shape != "silent") painter.drawArc(QRectF(18, 6, 17, 24), -70 * 16, 140 * 16); else { painter.drawLine(27, 13, 35, 23); painter.drawLine(27, 23, 35, 13); } + if (shape == "output") { painter.restore(); labelRect = QRectF(1, 23, 38, 13); labelSize = 9; } } else { painter.drawRoundedRect(QRectF(3, 5, 34, 26), 4, 4); if (!detail.isEmpty()) labelRect = QRectF(3, 16, 34, 15); diff --git a/tools/chatgui/tests/test_call_commands.cpp b/tools/chatgui/tests/test_call_commands.cpp index c1501585..44bf3788 100644 --- a/tools/chatgui/tests/test_call_commands.cpp +++ b/tools/chatgui/tests/test_call_commands.cpp @@ -2,12 +2,17 @@ * Linux hooks наблюдают границу native trampoline и затем вызывают его без подмены освобождения payload. */ #include "../src/callwindow.h" #include "../src/call_camera_capture.h" +#include "../src/call_video_engine.h" #include "../transport/gui_bridge.h" #include #include #include #include #include +#include +#include +#include +#include #include #include #include @@ -154,6 +159,65 @@ private slots: observed.clear(); bindingValid = true; QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); } + void videoSettingsMenus() { + CallWindow window; window.setCallId(77); window.setPeerName(QStringLiteral("Video call")); window.setState(CallWindow::Active); + auto* engine = window.findChild(); QVERIFY(engine); + auto* capture = engine->findChild(); QVERIFY(capture); + QObject::disconnect(capture, nullptr, engine, nullptr); // Детерминированные режимы, без открытия камеры/звонка. + CallCameraDevice first{QStringLiteral("Main camera"), QByteArray("main"), {}}; + stream_video_add_camera_modes(first.modes, QSize(1920, 1080), 29.97, 29.97); + stream_video_add_camera_modes(first.modes, QSize(1280, 720), 15, 60); + CallCameraDevice second{QStringLiteral("Secondary camera"), QByteArray("secondary"), {}}; + stream_video_add_camera_modes(second.modes, QSize(640, 480), 7, 7); + emit engine->camerasChanged({first, second}); + auto getMenu = [&window](const char* name) { return window.findChild(name); }; + auto chosen = [](QMenu* menu) { auto* action = menu->findChild()->checkedAction(); return action ? action->data() : QVariant(); }; + auto choose = [](QMenu* menu, const QVariant& value) { + for (auto* action : menu->actions()) if (action->isCheckable() && action->data() == value) { action->trigger(); return; } + QFAIL("Missing video setting"); + }; + auto* camera = getMenu("callVideoCamera"); auto* resolution = getMenu("callVideoResolution"); auto* fps = getMenu("callVideoFps"); + QVERIFY(camera); QVERIFY(resolution); QVERIFY(fps); QCOMPARE(chosen(camera).toByteArray(), first.id); + choose(resolution, QSize(1280, 720)); choose(fps, 60); QCOMPARE(chosen(fps).toInt(), 60); + const QImage previousResolution = resolution->menuAction()->icon().pixmap(40, 36).toImage(); + choose(resolution, QSize(1920, 1080)); QCOMPARE(chosen(fps).toInt(), 30); + QVERIFY(resolution->menuAction()->icon().pixmap(40, 36).toImage() != previousResolution); + for (auto* action : fps->actions()) if (action->isCheckable()) + QCOMPARE(action->isEnabled(), action->data().toInt() <= 30); + choose(camera, second.id); QCOMPARE(chosen(resolution).toSize(), QSize(640, 480)); QCOMPARE(chosen(fps).toInt(), 7); + emit engine->camerasChanged({second, first}); QCOMPARE(chosen(camera).toByteArray(), second.id); QCOMPARE(chosen(fps).toInt(), 7); + choose(camera, first.id); choose(resolution, QSize(1920, 1080)); choose(getMenu("callVideoCodec"), 1); + auto* minimum = window.findChild("callVideoMinimumBitrate"); QVERIFY(minimum); + auto* maximum = window.findChild("callVideoMaximumBitrate"); QVERIFY(maximum); + minimum->setValue(2000); maximum->setValue(1000); QCOMPARE(minimum->value(), 1000); + minimum->setValue(300); maximum->setValue(4000); + auto* settings = window.findChild("callVideoSettings"); QVERIFY(settings); + settings->click(); window.show(); QTest::qWait(20); + auto* bar = window.findChild("callVideoControls"); QVERIFY(bar); QVERIFY(bar->isVisible()); + QRect previous; + for (auto* action : bar->actions()) { + QRect button = bar->actionGeometry(action); QCOMPARE(button.size(), QSize(48, 48)); + if (!previous.isNull()) QCOMPARE(button.left() - previous.right() - 1, 4); + previous = button; QVERIFY(bar->rect().contains(button)); + QVERIFY(!action->menu()->actions().first()->text().isEmpty()); QVERIFY(!action->toolTip().isEmpty()); + } + const QString screenshot = qEnvironmentVariable("UTUN_TEST_CALL_CONTROLS_SCREENSHOT"); + if (!screenshot.isEmpty()) QVERIFY(window.grab().save(screenshot)); + auto* action = camera->menuAction(); const QIcon original = action->icon(); + QPixmap marker(80, 72); marker.fill(Qt::magenta); action->setIcon(QIcon(marker)); + const QImage rendered = bar->grab().toImage(); QRect iconBounds; + for (int y = 0; y < rendered.height(); y++) for (int x = 0; x < rendered.width(); x++) { + QColor color = rendered.pixelColor(x, y); + if (color.red() > 240 && color.blue() > 240 && color.green() < 15) iconBounds |= QRect(x, y, 1, 1); + } + action->setIcon(original); QCOMPARE(iconBounds.size(), QSize(40, 36) * rendered.devicePixelRatio()); + QTest::mouseClick(bar, Qt::LeftButton, {}, bar->actionGeometry(resolution->menuAction()).center()); QTRY_VERIFY(resolution->isVisible()); + QTest::mouseClick(bar, Qt::LeftButton, {}, bar->actionGeometry(fps->menuAction()).center()); + QTRY_VERIFY(fps->isVisible()); QVERIFY(!resolution->isVisible()); + QTest::mouseClick(bar, Qt::LeftButton, {}, bar->actionGeometry(fps->menuAction()).center()); QTRY_VERIFY(!fps->isVisible()); + settings->click(); QVERIFY(!bar->isVisible()); window.setState(CallWindow::Ended); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "call settings verified: rendered_icon=40x36 buttons=48x48 spacing=4 source/FPS/menu switching passed"); + } void windowAction_data() { QTest::addColumn("scenario"); QTest::addColumn("action"); QStringList failures{"unavailable", "stopped", "cancelled", "executed"};