Share capture control menus and Android style glyphs between desktop windows

This commit is contained in:
evgeny committed 2026-10-28 14:02:39 +01:00
1 parent 7be5c8bd0b
commit f9c150b7d4
2 files changed
+132 -112

No files matched your search

+129
View File
@@ -0,0 +1,129 @@
/* Общие кнопки настроек захвата для звонка и трансляции: 48x48, значок 40x36, зазор 4.
* MenuStyle назначается QMenuBar, родитель стиля должен пережить bar и его меню.
* Меню вариантов должно содержать один QActionGroup; data хранит значение, checked — текущий выбор.
* select меняет выбор без triggered; addOption отмечает первый вариант, clearOptions сохраняет группу и добавляет заголовок.
* controlIcon рисует выбранное значение внутри силуэта, detail — дополнительную короткую подпись, rotation — угол маркера. */
#pragma once
#include <QCoreApplication>
#include <QMenuBar>
#include <QMenu>
#include <QActionGroup>
#include <QProxyStyle>
#include <QStyleOptionMenuItem>
#include <QPainter>
#include <QPainterPath>
namespace CaptureControls {
class MenuStyle : 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 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();
}
};
inline QAction* selected(QMenu* menu) { return menu->findChild<QActionGroup*>()->checkedAction(); }
inline QVariant value(QMenu* menu) { auto* action = selected(menu); return action ? action->data() : QVariant(); }
inline QAction* option(QMenu* menu, const QVariant& data) {
for (auto* action : menu->actions()) if (action->isCheckable() && action->data() == data) return action;
return nullptr;
}
inline bool select(QMenu* menu, const QVariant& data) {
auto* action = option(menu, data);
if (action) action->setChecked(true);
return action;
}
inline QAction* addOption(QMenu* menu, const QString& label, const QVariant& data) {
auto* group = menu->findChild<QActionGroup*>();
auto* action = menu->addAction(label); action->setCheckable(true); action->setData(data); group->addAction(action);
if (!group->checkedAction()) action->setChecked(true);
return action;
}
inline void clearOptions(QMenu* menu, const char* heading) { menu->clear(); menu->addSection(QCoreApplication::translate("ChatGui", heading)); }
/* Компактные векторные значки с текущим значением, читаемые в обеих темах. */
inline 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") {
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(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);
if (shape == "sound") painter.drawArc(QRectF(18, 6, 17, 24), -70 * 16, 140 * 16);
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(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);
}
}
+3 -112
View File
@@ -1,4 +1,5 @@
#include "stream_publish_window.h"
#include "capture_controls.h"
#include "sound_manager.h"
#include "debug_config.h"
#include <QVBoxLayout>
@@ -32,120 +33,10 @@
namespace {
constexpr int MaximumManualBitrate = 12000000;
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 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"; }
QAction* selected(QMenu* menu) { return menu->findChild<QActionGroup*>()->checkedAction(); }
QVariant value(QMenu* menu) { auto* action = selected(menu); return action ? action->data() : QVariant(); }
QAction* option(QMenu* menu, const QVariant& data) {
for (auto* action : menu->actions()) if (action->isCheckable() && action->data() == data) return action;
return nullptr;
}
bool select(QMenu* menu, const QVariant& data) {
auto* action = option(menu, data);
if (action) action->setChecked(true);
return action;
}
QAction* addOption(QMenu* menu, const QString& label, const QVariant& data) {
auto* group = menu->findChild<QActionGroup*>();
auto* action = menu->addAction(label); action->setCheckable(true); action->setData(data); group->addAction(action);
if (!group->checkedAction()) action->setChecked(true);
return action;
}
void clearOptions(QMenu* menu, const char* heading) { menu->clear(); menu->addSection(text(heading)); }
/* Компактные векторные значки с текущим значением, читаемые в обеих темах. */
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") {
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(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);
if (shape == "sound") painter.drawArc(QRectF(18, 6, 17, 24), -70 * 16, 140 * 16);
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(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);
}
using namespace CaptureControls;
}
/* Горизонталь — сохранённое окно источника, одна линия объединяет субпотоки получателя. */
@@ -216,7 +107,7 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g
auto* layout = new QVBoxLayout(this);
auto* controls = new QHBoxLayout; controls->setSpacing(4);
m_menuBar = new QMenuBar(this); m_menuBar->setNativeMenuBar(false); m_menuBar->setMouseTracking(true);
auto* menuStyle = new StreamMenuStyle; menuStyle->setParent(this); m_menuBar->setStyle(menuStyle);
auto* menuStyle = new MenuStyle; menuStyle->setParent(this); m_menuBar->setStyle(menuStyle);
m_menuBar->setObjectName(QStringLiteral("streamControls"));
auto makeMenu = [this](const char* heading, const char* name) {
auto* menu = m_menuBar->addMenu(QString()); menu->setObjectName(QString::fromLatin1(name));