Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/gui/ModemChrome.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -396,12 +396,15 @@ QScrollBar::sub-line:vertical {
// The canon look's check boxes and radio buttons are the painted canon
// indicators alone: the sheet's own indicator rules come out first, since
// its :checked ones are more specific than the generic indicator rule and
// would draw a border around the image. Lists (the AetherRX/AetherTX
// profile libraries) sit on a nested card; the modem look leaves them to
// the application sheet.
// would draw a border around the image. A rule body here holds {{token}}
// placeholders, so it ends at the first brace outside one: ending at the
// first "}" would stop inside a token and leave the rest of the rule
// behind, breaking the sheet so the painted indicators never apply. Lists
// (the AetherRX/AetherTX profile libraries) sit on a nested card; the
// modem look leaves them to the application sheet.
if (look == Look::Canon) {
static const QRegularExpression ownIndicatorRules(
QStringLiteral(R"((?:QCheckBox|QRadioButton)::indicator[^{]*\{[^}]*\}\s*)"));
static const QRegularExpression ownIndicatorRules(QStringLiteral(
R"((?:QCheckBox|QRadioButton)::indicator[^{]*\{(?:\{\{[^{}]*\}\}|[^{}])*\}\s*)"));
sheet.remove(ownIndicatorRules);
sheet += canonIndicatorRules();
sheet += QStringLiteral(
Expand Down
72 changes: 72 additions & 0 deletions tests/modem_chrome_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,18 @@
#include "TestSettingsProfile.h"

#include <QApplication>
#include <QCheckBox>
#include <QColor>
#include <QImage>
#include <QRadioButton>
#include <QStyle>
#include <QStyleOptionButton>
#include <QRegularExpression>
#include <QString>
#include <QtTest>

#include <cstdio>
#include <type_traits>

using namespace AetherSDR;

Expand All @@ -38,6 +45,8 @@ private slots:
void everyThemeTokenInTheSheetResolves();
void checkedLosesToDisabled();
void canonLookPaintsItsIndicators();
void canonSheetLeavesNoRuleFragments();
void canonIndicatorsRenderTheirImages();
};

namespace {
Expand All @@ -51,6 +60,29 @@ int baseFontSize(const QString& sheet)
return m.hasMatch() ? m.captured(1).toInt() : -1;
}

// The pixels of `button`'s indicator under `sheet` (theme-resolved), so two
// sheets can be compared by what they draw rather than by their text.
template <typename Button>
QImage indicatorPixels(const QString& sheet, bool checked)
{
Button button(QStringLiteral("x"));
button.setStyleSheet(ThemeManager::instance().resolve(sheet));
button.setChecked(checked);
button.ensurePolished();
button.resize(button.sizeHint());
QStyleOptionButton option;
option.initFrom(&button);
const auto element = std::is_same_v<Button, QCheckBox> ? QStyle::SE_CheckBoxIndicator
: QStyle::SE_RadioButtonIndicator;
const QRect rect = button.style()->subElementRect(element, &option, &button);
// Over one fixed backdrop, without the widget's own background, so only
// what the indicator draws can differ.
QImage pixels(rect.size(), QImage::Format_ARGB32_Premultiplied);
pixels.fill(QColor(0x40, 0x20, 0x60));
button.render(&pixels, QPoint(), QRegion(rect), QWidget::DrawChildren);
return pixels;
}

} // namespace

void ModemChromeTest::bothScalesCarryEveryStructuralSelector()
Expand Down Expand Up @@ -134,6 +166,46 @@ void ModemChromeTest::canonLookPaintsItsIndicators()
}
}

// Taking the sheet's own indicator rules out must take each whole rule. Their
// bodies hold {{token}} placeholders, and a rule cut at the first "}" leaves
// "}};" fragments that break the sheet, so the painted indicators never apply.
void ModemChromeTest::canonSheetLeavesNoRuleFragments()
{
static const QRegularExpression token(QStringLiteral(R"(\{\{[^{}]*\}\})"));
for (const auto scale : {ModemChrome::Scale::Dialog, ModemChrome::Scale::Compact}) {
QString structure = ModemChrome::styleSheet(scale, ModemChrome::Look::Canon);
structure.replace(token, QStringLiteral("token"));
int depth = 0;
for (const QChar ch : structure) {
if (ch == QLatin1Char('{')) {
QVERIFY2(++depth == 1, "a rule opens inside another rule");
} else if (ch == QLatin1Char('}')) {
QVERIFY2(--depth == 0, "a closing brace has no rule to close");
}
}
QCOMPARE(depth, 0);
}
}

// In the canon look a check box and a radio button draw exactly what the
// painted canon indicators draw on their own, checked and unchecked: the rest
// of the sheet neither hides the images nor draws over them.
void ModemChromeTest::canonIndicatorsRenderTheirImages()
{
const QString indicators = canonIndicatorRules();
QVERIFY(!indicators.isEmpty());
const QString canon = ModemChrome::styleSheet(ModemChrome::Scale::Dialog,
ModemChrome::Look::Canon);
for (const bool checked : {false, true}) {
const QImage box = indicatorPixels<QCheckBox>(canon, checked);
QVERIFY(!box.isNull());
QCOMPARE(box, indicatorPixels<QCheckBox>(indicators, checked));
const QImage radio = indicatorPixels<QRadioButton>(canon, checked);
QVERIFY(!radio.isNull());
QCOMPARE(radio, indicatorPixels<QRadioButton>(indicators, checked));
}
}

void ModemChromeTest::compactIsSmallerButNotADifferentPalette()
{
const QString dialog = ModemChrome::styleSheet(ModemChrome::Scale::Dialog);
Expand Down
Loading