Render broadcast controls at their actual icon size with Android style values and rotation
This commit is contained in:
1 parent
01dd40fdd1
commit
26953fc1a4
2 files changed
+96
-23
No files matched your search
@@ -15,7 +15,9 @@
|
||||
#include <QWidgetAction>
|
||||
#include <QToolButton>
|
||||
#include <QProxyStyle>
|
||||
#include <QStyleOptionMenuItem>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QSignalBlocker>
|
||||
#include <QJsonDocument>
|
||||
#include <QScopedValueRollback>
|
||||
@@ -34,11 +36,29 @@ class StreamMenuStyle : public QProxyStyle {
|
||||
public:
|
||||
int pixelMetric(PixelMetric metric, const QStyleOption* option, const QWidget* widget) const override {
|
||||
if (qobject_cast<const QMenuBar*>(widget)) {
|
||||
if (metric == PM_SmallIconSize) return 72;
|
||||
if (metric == PM_MenuBarItemSpacing) return 0;
|
||||
if (metric == PM_SmallIconSize) return 40;
|
||||
if (metric == PM_MenuBarItemSpacing) return 4;
|
||||
}
|
||||
return QProxyStyle::pixelMetric(metric, option, widget);
|
||||
}
|
||||
QSize sizeFromContents(ContentsType type, const QStyleOption* option, const QSize& size, const QWidget* widget) const override {
|
||||
if (type == CT_MenuBarItem && qobject_cast<const QMenuBar*>(widget)) return QSize(48, 48);
|
||||
return QProxyStyle::sizeFromContents(type, option, size, widget);
|
||||
}
|
||||
void drawControl(ControlElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget) const override {
|
||||
auto* item = qstyleoption_cast<const QStyleOptionMenuItem*>(option);
|
||||
if (element != CE_MenuBarItem || !item || !qobject_cast<const QMenuBar*>(widget)) {
|
||||
QProxyStyle::drawControl(element, option, painter, widget); return;
|
||||
}
|
||||
painter->save(); painter->setRenderHint(QPainter::Antialiasing); painter->setPen(Qt::NoPen);
|
||||
QColor background = item->palette.button().color();
|
||||
if (item->state & (State_Selected | State_Sunken)) background = item->palette.highlight().color();
|
||||
background.setAlphaF(item->state & State_Enabled ? 0.3 : 0.12); painter->setBrush(background);
|
||||
painter->drawRoundedRect(item->rect, 10, 10);
|
||||
QRect iconRect = alignedRect(item->direction, Qt::AlignCenter, QSize(40, 36), item->rect);
|
||||
item->icon.paint(painter, iconRect, Qt::AlignCenter, item->state & State_Enabled ? QIcon::Normal : QIcon::Disabled);
|
||||
painter->restore();
|
||||
}
|
||||
};
|
||||
QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); }
|
||||
QString bytes(uint64_t value) { return QString::number(value / 1024.0, 'f', 1) + " KiB"; }
|
||||
@@ -63,21 +83,49 @@ QAction* addOption(QMenu* menu, const QString& label, const QVariant& data) {
|
||||
void clearOptions(QMenu* menu, const char* heading) { menu->clear(); menu->addSection(text(heading)); }
|
||||
|
||||
/* Компактные векторные значки с текущим значением, читаемые в обеих темах. */
|
||||
QIcon controlIcon(const QPalette& palette, const QString& shape, const QString& label = {}) {
|
||||
QIcon controlIcon(const QPalette& palette, const QString& shape, const QString& label = {}, const QString& detail = {}, int rotation = 0) {
|
||||
QPixmap pixmap(80, 72); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); painter.scale(2, 2);
|
||||
painter.setRenderHint(QPainter::Antialiasing); painter.setPen(QPen(palette.windowText(), 1.6));
|
||||
QRectF labelRect(3, 5, 34, 26); int labelSize = 11;
|
||||
if (shape == "camera") {
|
||||
painter.drawRoundedRect(QRectF(5, 9, 30, 21), 3, 3); painter.drawEllipse(QPointF(20, 19), 6, 6);
|
||||
painter.drawLine(12, 8, 15, 5); painter.drawLine(15, 5, 25, 5); painter.drawLine(25, 5, 28, 8);
|
||||
QPainterPath body; body.moveTo(4, 9); body.lineTo(11, 9); body.lineTo(14, 3); body.lineTo(26, 3);
|
||||
body.lineTo(29, 9); body.lineTo(36, 9); body.quadTo(38, 9, 38, 12); body.lineTo(38, 31);
|
||||
body.quadTo(38, 34, 35, 34); body.lineTo(5, 34); body.quadTo(2, 34, 2, 31); body.lineTo(2, 12); body.quadTo(2, 9, 4, 9);
|
||||
painter.drawPath(body); labelRect.moveTop(10); labelRect.setHeight(22);
|
||||
} else if (shape == "screen") {
|
||||
painter.drawRoundedRect(QRectF(3, 4, 34, 25), 2, 2); painter.drawLine(20, 29, 20, 33); painter.drawLine(12, 33, 28, 33);
|
||||
painter.drawRoundedRect(QRectF(1.5, 2, 37, 27), 3, 3); painter.drawLine(20, 29, 20, 34); painter.drawLine(12, 34, 28, 34);
|
||||
labelRect = QRectF(3, 3, 34, 24);
|
||||
} else if (shape == "chip") {
|
||||
painter.drawRoundedRect(QRectF(5.5, 5, 29, 26), 3, 3);
|
||||
painter.setPen(QPen(palette.highlight(), 1.5));
|
||||
for (double position : {0.28, 0.5, 0.72}) {
|
||||
painter.drawLine(QPointF(40 * position, 1), QPointF(40 * position, 5));
|
||||
painter.drawLine(QPointF(40 * position, 31), QPointF(40 * position, 35));
|
||||
painter.drawLine(QPointF(1, 36 * position), QPointF(5.5, 36 * position));
|
||||
painter.drawLine(QPointF(34.5, 36 * position), QPointF(39, 36 * position));
|
||||
}
|
||||
labelRect = QRectF(6, 5, 28, 26);
|
||||
} else if (shape == "gauge") {
|
||||
painter.setPen(QPen(palette.highlight(), 1.5)); painter.drawArc(QRectF(2, 2, 36, 32), 0, 180 * 16);
|
||||
painter.drawLine(2, 18, 6, 18); painter.drawLine(20, 2, 20, 6); painter.drawLine(34, 18, 38, 18);
|
||||
painter.drawLine(6, 34, 34, 34); labelRect = QRectF(3, 17, 34, 15); labelSize = 12;
|
||||
} else if (shape == "rotation") {
|
||||
painter.save(); painter.translate(20, 18); painter.rotate(rotation); painter.translate(-20, -18);
|
||||
QColor frame = palette.windowText().color(); frame.setAlphaF(0.6); painter.setPen(QPen(frame, 1.5));
|
||||
painter.drawRoundedRect(QRectF(8, 1.5, 24, 33), 4, 4);
|
||||
painter.setPen(Qt::NoPen); painter.setBrush(palette.highlight()); painter.drawRoundedRect(QRectF(8, 1.5, 24, 4), 2, 2);
|
||||
QPolygonF triangle; triangle << QPointF(15, 4.5) << QPointF(25, 4.5) << QPointF(20, 8.5);
|
||||
painter.drawPolygon(triangle); painter.restore(); labelSize = 10;
|
||||
labelRect.translate(rotation == 90 ? -2 : rotation == 270 ? 2 : 0, rotation == 0 ? 2 : 0);
|
||||
} else if (shape == "settings") {
|
||||
painter.translate(20, 18);
|
||||
for (int i = 0; i < 8; i++) { painter.drawLine(0, -10, 0, -15); painter.rotate(45); }
|
||||
painter.drawEllipse(QPointF(), 10, 10); painter.drawEllipse(QPointF(), 4, 4);
|
||||
} else if (shape == "microphone") {
|
||||
painter.save(); painter.translate(7, 0); painter.scale(0.65, 0.65);
|
||||
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") {
|
||||
QPolygon speaker; speaker << QPoint(6, 14) << QPoint(13, 14) << QPoint(22, 7) << QPoint(22, 29)
|
||||
<< QPoint(13, 22) << QPoint(6, 22); painter.drawPolygon(speaker);
|
||||
@@ -85,10 +133,16 @@ QIcon controlIcon(const QPalette& palette, const QString& shape, const QString&
|
||||
else { painter.drawLine(27, 13, 35, 23); painter.drawLine(27, 23, 35, 13); }
|
||||
} else {
|
||||
painter.drawRoundedRect(QRectF(3, 5, 34, 26), 4, 4);
|
||||
if (!detail.isEmpty()) labelRect = QRectF(3, 16, 34, 15);
|
||||
}
|
||||
painter.setPen(palette.windowText().color());
|
||||
if (!label.isEmpty()) {
|
||||
QFont font = painter.font(); font.setPixelSize(label.size() > 4 ? 10 : 12); font.setBold(true); painter.setFont(font);
|
||||
painter.drawText(QRectF(3, 5, 34, 26), Qt::AlignCenter, label);
|
||||
QFont font = painter.font(); font.setPixelSize(labelSize); font.setBold(true); painter.setFont(font);
|
||||
painter.drawText(labelRect, Qt::AlignCenter, painter.fontMetrics().elidedText(label, Qt::ElideRight, int(labelRect.width())));
|
||||
}
|
||||
if (!detail.isEmpty()) {
|
||||
QFont font = painter.font(); font.setPixelSize(8); font.setBold(false); painter.setFont(font);
|
||||
painter.drawText(QRectF(3, 7, 34, 10), Qt::AlignCenter, detail);
|
||||
}
|
||||
return QIcon(pixmap);
|
||||
}
|
||||
@@ -163,7 +217,6 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g
|
||||
m_menuBar = new QMenuBar(this); m_menuBar->setNativeMenuBar(false); m_menuBar->setMouseTracking(true);
|
||||
auto* menuStyle = new StreamMenuStyle; menuStyle->setParent(this); m_menuBar->setStyle(menuStyle);
|
||||
m_menuBar->setObjectName(QStringLiteral("streamControls"));
|
||||
m_menuBar->setStyleSheet("QMenuBar { background: transparent; } QMenuBar::item { padding: 2px; }");
|
||||
auto makeMenu = [this](const char* heading, const char* name) {
|
||||
auto* menu = m_menuBar->addMenu(QString()); menu->setObjectName(QString::fromLatin1(name));
|
||||
menu->menuAction()->setToolTip(text(heading)); menu->setAccessibleName(text(heading));
|
||||
@@ -180,7 +233,7 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g
|
||||
auto* settingsMenu = makeMenu("Settings", "streamExtraSettings");
|
||||
settingsMenu->menuAction()->setIcon(controlIcon(palette(), "settings"));
|
||||
controls->addWidget(m_menuBar);
|
||||
m_sound = new QToolButton(this); m_sound->setCheckable(true); m_sound->setIconSize(QSize(80, 72));
|
||||
m_sound = new QToolButton(this); m_sound->setCheckable(true); m_sound->setIconSize(QSize(40, 36)); m_sound->setFixedSize(48, 48);
|
||||
m_sound->setObjectName(QStringLiteral("streamSound")); m_sound->setAutoRaise(true); controls->addWidget(m_sound);
|
||||
controls->addStretch();
|
||||
m_start = new QPushButton(text("Start stream"), this); m_start->setMinimumHeight(40);
|
||||
@@ -208,10 +261,7 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop manual stream bitrate selection maximum_bps=%d", MaximumManualBitrate);
|
||||
for (int angle : {0, 90, 270}) {
|
||||
auto* action = addOption(m_rotation, QStringLiteral("%1°").arg(angle), angle);
|
||||
QPixmap icon(20, 20); icon.fill(Qt::transparent); QPainter painter(&icon);
|
||||
painter.translate(10, 10); painter.rotate(-angle); painter.setPen(QPen(QColor("#e53935"), 3)); painter.drawLine(-6, -6, 6, -6);
|
||||
painter.setPen(QPen(palette().color(QPalette::Text), 1)); painter.drawLine(-6, -4, -6, 6); painter.drawLine(6, -4, 6, 6); painter.drawLine(-6, 6, 6, 6);
|
||||
painter.end(); action->setIcon(QIcon(icon)); action->setToolTip(text("The red edge marks the top for viewers"));
|
||||
action->setIcon(controlIcon(palette(), "rotation", action->text(), {}, angle));
|
||||
}
|
||||
m_cursor = new QCheckBox(text("Show mouse cursor"), settings); m_cursor->setChecked(true);
|
||||
m_cursor->setObjectName(QStringLiteral("streamShowCursor")); form->addRow(m_cursor);
|
||||
@@ -290,7 +340,7 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g
|
||||
.arg(bytes(stats.video_bytes), bytes(stats.inflight_bytes), bytes(stats.total_bytes), bytes(stats.acked_1s)));
|
||||
});
|
||||
refreshSources(); refreshCodecs(); restoreSettings(); synchronize();
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop publisher control row icon=80x72 menu_icon=72 spacing=4 minimum_width=%d", minimumWidth());
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop publisher control row rendered_icon=40x36 button=48x48 spacing=4 minimum_width=%d", minimumWidth());
|
||||
}
|
||||
|
||||
void StreamPublishWindow::closeEvent(QCloseEvent* event) {
|
||||
@@ -534,22 +584,24 @@ void StreamPublishWindow::synchronize() {
|
||||
}
|
||||
|
||||
void StreamPublishWindow::updateMenuButtons() {
|
||||
auto update = [this](QMenu* menu, const char* heading, const QString& shape, const QString& label = {}) {
|
||||
menu->menuAction()->setIcon(controlIcon(palette(), shape, label));
|
||||
auto update = [this](QMenu* menu, const char* heading, const QString& shape, const QString& label = {}, const QString& detail = {}, int rotation = 0) {
|
||||
menu->menuAction()->setIcon(controlIcon(palette(), shape, label, detail, rotation));
|
||||
QString description = text(heading);
|
||||
if (auto* action = selected(menu)) description += QStringLiteral(": ") + action->text();
|
||||
menu->menuAction()->setToolTip(description); menu->setAccessibleName(description);
|
||||
};
|
||||
auto config = selection();
|
||||
update(m_source, "Video source", config.source.kind == StreamVideoSource::Camera ? "camera" : "screen");
|
||||
update(m_source, "Video source", config.source.kind == StreamVideoSource::Camera ? "camera" : "screen", config.source.name);
|
||||
update(m_codec, "Video codec", "chip", selected(m_codec) ? LiveVideoEncoder::codecName(config.video.codec).remove('.') : "—");
|
||||
update(m_bitrate, "Video bitrate", "gauge", config.automaticBitrate ? "Auto" : QString::number(config.video.bitrate / 1000000.0, 'g', 2));
|
||||
QString bitrate = config.video.bitrate >= 1000000 ? QString::number(config.video.bitrate / 1000000.0, 'g', 2) + 'M'
|
||||
: QString::number(config.video.bitrate / 1000) + 'k';
|
||||
update(m_bitrate, "Video bitrate", "gauge", bitrate, config.automaticBitrate ? "A" : QString());
|
||||
QSize size = value(m_resolution).toSize();
|
||||
update(m_resolution, "Video resolution", "screen", size.isEmpty() ? "Auto" : QString::number(std::min(size.width(), size.height())) + 'p');
|
||||
update(m_fps, "Frame rate", "fps", selected(m_fps) ? QString::number(config.video.fps) : "—");
|
||||
update(m_rotation, "Rotation", "rotation", QStringLiteral("%1°").arg(config.video.rotation));
|
||||
update(m_maxQp, "Maximum QP", "qp", value(m_maxQp).toInt() ? value(m_maxQp).toString() : "Auto");
|
||||
update(m_microphone, "Audio source", config.audio ? "microphone" : "silent");
|
||||
update(m_fps, "Frame rate", "fps", selected(m_fps) ? QString::number(config.video.fps) : "—", "FPS");
|
||||
update(m_rotation, "Rotation", "rotation", QStringLiteral("%1°").arg(config.video.rotation), {}, config.video.rotation);
|
||||
update(m_maxQp, "Maximum QP", "qp", value(m_maxQp).toInt() ? value(m_maxQp).toString() : "Auto", "QP");
|
||||
update(m_microphone, "Audio source", config.audio ? "microphone" : "silent", config.audio ? selected(m_microphone)->text() : QString());
|
||||
m_sound->setIcon(controlIcon(palette(), m_sound->isChecked() ? "sound" : "silent"));
|
||||
m_sound->setToolTip(text(m_sound->isChecked() ? "Turn stream audio off" : "Turn stream audio on"));
|
||||
m_sound->setAccessibleName(m_sound->toolTip());
|
||||
|
||||
@@ -463,6 +463,27 @@ private slots:
|
||||
auto* start = controls.findChild<QPushButton*>("streamStart"); QVERIFY(start);
|
||||
auto* sound = controls.findChild<QToolButton*>("streamSound"); QVERIFY(sound);
|
||||
auto* buffer = controls.findChild<QWidget*>("streamBufferIndicator"); QVERIFY(buffer);
|
||||
const QString screenshot = qEnvironmentVariable("UTUN_TEST_STREAM_CONTROLS_SCREENSHOT");
|
||||
if (!screenshot.isEmpty()) QVERIFY(controls.grab().save(screenshot));
|
||||
QRect previous;
|
||||
for (auto* action : bar->actions()) {
|
||||
const QRect button = bar->actionGeometry(action); QCOMPARE(button.size(), QSize(48, 48));
|
||||
if (!previous.isNull()) QCOMPARE(button.left() - previous.right() - 1, 4);
|
||||
previous = button;
|
||||
}
|
||||
auto* sourceAction = menu(controls, "streamVideoSource")->menuAction();
|
||||
const QIcon sourceIcon = sourceAction->icon();
|
||||
QPixmap marker(80, 72); marker.fill(Qt::magenta); sourceAction->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);
|
||||
}
|
||||
sourceAction->setIcon(sourceIcon);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop stream rendered icon=%dx%d sound_icon=%dx%d scale=%.2f",
|
||||
iconBounds.width(), iconBounds.height(), sound->iconSize().width(), sound->iconSize().height(), rendered.devicePixelRatio());
|
||||
QCOMPARE(iconBounds.size(), QSize(40, 36) * rendered.devicePixelRatio());
|
||||
QCOMPARE(sound->iconSize(), QSize(40, 36));
|
||||
QCOMPARE(buffer->height(), 20); QCOMPARE(buffer->width(), std::max(80, start->sizeHint().width()));
|
||||
QCOMPARE(buffer->geometry().center().y(), start->geometry().center().y());
|
||||
QVERIFY(std::abs(bar->geometry().center().y() - start->geometry().center().y()) <= 1);
|
||||
|
||||
Reference in new issue
Block a user