diff --git a/src/gui/ModemChrome.cpp b/src/gui/ModemChrome.cpp index 18de184907..ce6cb8bb45 100644 --- a/src/gui/ModemChrome.cpp +++ b/src/gui/ModemChrome.cpp @@ -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( diff --git a/tests/modem_chrome_test.cpp b/tests/modem_chrome_test.cpp index 50d982e6ac..5fe1ca33d3 100644 --- a/tests/modem_chrome_test.cpp +++ b/tests/modem_chrome_test.cpp @@ -19,11 +19,18 @@ #include "TestSettingsProfile.h" #include +#include +#include +#include +#include +#include +#include #include #include #include #include +#include using namespace AetherSDR; @@ -38,6 +45,8 @@ private slots: void everyThemeTokenInTheSheetResolves(); void checkedLosesToDisabled(); void canonLookPaintsItsIndicators(); + void canonSheetLeavesNoRuleFragments(); + void canonIndicatorsRenderTheirImages(); }; namespace { @@ -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 +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 ? 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() @@ -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(canon, checked); + QVERIFY(!box.isNull()); + QCOMPARE(box, indicatorPixels(indicators, checked)); + const QImage radio = indicatorPixels(canon, checked); + QVERIFY(!radio.isNull()); + QCOMPARE(radio, indicatorPixels(indicators, checked)); + } +} + void ModemChromeTest::compactIsSmallerButNotADifferentPalette() { const QString dialog = ModemChrome::styleSheet(ModemChrome::Scale::Dialog);