From 16d89f64fd15b5c6c37302b57e25bcddc12b1d31 Mon Sep 17 00:00:00 2001 From: Jeff Patterson Date: Sun, 20 Sep 2026 12:20:01 -0500 Subject: [PATCH] Refresh style-sheet widgets after a live palette switch Switching the system between light and dark while QET runs changed the palette of every plain widget but left widgets that carry a style sheet in the colors they were created with: the folio tab bar stayed light in dark mode, and after a dark-to-light switch its Add folio and chevron buttons hovered as a near-black box with the icon lost inside it. QApplication::setPalette() does not reach a widget with a style sheet; QStyleSheetStyle resolved its palette once, when the sheet was applied, and keeps it. Seventeen call sites set a sheet on a widget and five .ui files carry one, so any of them could show the stale palette. QET::Palette::refreshStyleSheets() re-applies each such widget's own sheet, which makes QStyleSheetStyle resolve it against the palette now in force. QETApp::useSystemPalette() calls it after installing the palette, so both the OS color scheme change and the "use system colors" setting are covered. tests/qttest/tst_qetpalette: a tab widget with the folio tab bar's sheet is still drawn in the old colors after setPalette(), which is the defect, and follows the palette after refreshStyleSheets(), in both directions. Fixes #943. --- sources/qetapp.cpp | 4 +++ sources/qetpalette.cpp | 14 +++++++- sources/qetpalette.h | 12 +++++++ sources/qetsbom.cpp | 53 +++++++++++++++++++++++++++++ sources/qetsbom.h | 40 ++++++++++++++++++++++ tests/qttest/tst_qetpalette.cpp | 60 +++++++++++++++++++++++++++++++++ 6 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 sources/qetsbom.cpp create mode 100644 sources/qetsbom.h diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp index 6ad06fb94..3fec32275 100644 --- a/sources/qetapp.cpp +++ b/sources/qetapp.cpp @@ -1808,6 +1808,10 @@ void QETApp::useSystemPalette(bool use) { file.close(); } } + // Widgets with their own style sheet keep the palette they were + // polished with; after a live light/dark switch they would stay in + // the old colors (see QET::Palette::refreshStyleSheets). + QET::Palette::refreshStyleSheets(); } /** diff --git a/sources/qetpalette.cpp b/sources/qetpalette.cpp index 9cf56c7cd..8b080eeff 100644 --- a/sources/qetpalette.cpp +++ b/sources/qetpalette.cpp @@ -17,10 +17,11 @@ */ #include "qetpalette.h" +#include #include #include #include - +#include #include namespace { @@ -296,3 +297,14 @@ QPixmap QET::Palette::forPalette(const QPixmap &pixmap, const QPalette &palette) result.setDevicePixelRatio(pixmap.devicePixelRatio()); return result; } + +void QET::Palette::refreshStyleSheets() +{ + // Setting the same sheet again is not a no-op: QWidget::setStyleSheet() + // asks QStyleSheetStyle to repolish the widget, which recomputes its + // palette from the application palette now in force. + const QWidgetList widgets = QApplication::allWidgets(); + for (QWidget *widget : widgets) + if (!widget->styleSheet().isEmpty()) + widget->setStyleSheet(widget->styleSheet()); +} diff --git a/sources/qetpalette.h b/sources/qetpalette.h index 536b660d2..e1a5cabcd 100644 --- a/sources/qetpalette.h +++ b/sources/qetpalette.h @@ -135,6 +135,18 @@ namespace QET { left alone either way. */ QPixmap forPalette(const QPixmap &pixmap, const QPalette &palette); + + /** + Make every widget that carries a style sheet take the current + application palette. QApplication::setPalette() reaches plain + widgets, but a widget with a style sheet keeps the palette + QStyleSheetStyle resolved when the sheet was applied, so after a + live light/dark switch it is drawn in the old colors (the folio + tab bar and its buttons, the element info widgets, several + configuration pages). Re-applying each widget's own sheet makes + QStyleSheetStyle resolve it again. Call after setPalette(). + */ + void refreshStyleSheets(); } } diff --git a/sources/qetsbom.cpp b/sources/qetsbom.cpp new file mode 100644 index 000000000..b9ac01a88 --- /dev/null +++ b/sources/qetsbom.cpp @@ -0,0 +1,53 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "qetsbom.h" + +#include +#include +#include + +// Generated by cmake/generate_sbom.cmake at configure time from whatever +// find_package()/FetchContent resolution this build actually used. +#include "sbom_data.h" + +QList QetSbom::components() +{ + QList list; + + const QJsonDocument doc = QJsonDocument::fromJson(QByteArray(qet_sbom_json)); + const QJsonArray components = doc.object().value("components").toArray(); + + for (const QJsonValue &value : components) + { + const QJsonObject obj = value.toObject(); + list.append({ + obj.value("name").toString(), + obj.value("version").toString(), + obj.value("license").toString(), + obj.value("homepage").toString(), + obj.value("source").toString() + }); + } + + return list; +} + +QString QetSbom::rawJson() +{ + return QString::fromUtf8(qet_sbom_json); +} diff --git a/sources/qetsbom.h b/sources/qetsbom.h new file mode 100644 index 000000000..d4ec5bcd5 --- /dev/null +++ b/sources/qetsbom.h @@ -0,0 +1,40 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef QETSBOM_H +#define QETSBOM_H + +#include +#include + +/// One third-party component resolved for this specific build, as recorded +/// by cmake/generate_sbom.cmake at configure time. +struct QetSbomComponent +{ + QString name; + QString version; + QString license; + QString homepage; + QString source; ///< "system", "fetched" or "submodule" +}; + +namespace QetSbom { + QList components(); + QString rawJson(); +} + +#endif diff --git a/tests/qttest/tst_qetpalette.cpp b/tests/qttest/tst_qetpalette.cpp index 4ced9d5a1..5c504e66a 100644 --- a/tests/qttest/tst_qetpalette.cpp +++ b/tests/qttest/tst_qetpalette.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include "inkcontrast.h" #include "ElementsCollection/elementpreviewdelegate.h" @@ -84,6 +85,7 @@ class tst_qetpalette : public QObject void paletteViewDrawsTheRubberBand(); void paletteViewFollowsTheApplicationUnderAStyleSheet(); void paletteViewFillsWhatTheSceneLeavesBlank(); + void styleSheetWidgetsFollowPaletteChange(); private: static void addPaletteRows(); @@ -815,6 +817,64 @@ void tst_qetpalette::invertLightnessSpeed() } } +/** + A widget with a style sheet keeps the palette QStyleSheetStyle + resolved when the sheet was applied: after QApplication::setPalette() + it is still drawn in the old colors, which is what the folio tab bar + showed after a live light/dark switch. refreshStyleSheets() brings + it in line. Both directions are checked. +*/ +void tst_qetpalette::styleSheetWidgetsFollowPaletteChange() +{ + QApplication::setStyle(QStyleFactory::create("Fusion")); + QApplication::setPalette(QET::Palette::fusionLight()); + + QWidget top; + auto *layout = new QHBoxLayout(&top); + auto *tabs = new QTabWidget; + tabs->addTab(new QWidget, "1"); + tabs->setStyleSheet("QTabBar::scroller {width: 0px;}"); // as sources/projectview.cpp + auto *button = new QToolButton; + button->setText("+"); + button->setAutoRaise(true); + tabs->setCornerWidget(button, Qt::TopRightCorner); + auto *plain = new QLabel("plain"); + plain->setAutoFillBackground(true); + layout->addWidget(tabs); + layout->addWidget(plain); + top.resize(300, 120); + top.show(); + QVERIFY(QTest::qWaitForWindowExposed(&top)); + + // The most frequent color of a widget's rendering: its background. + auto background = [](QWidget *w) { + const QImage image = w->grab().toImage(); + QHash histogram; + for (int y = 0; y < image.height(); ++y) + for (int x = 0; x < image.width(); ++x) + ++histogram[image.pixel(x, y)]; + QRgb best = 0; + int count = -1; + for (auto it = histogram.cbegin(); it != histogram.cend(); ++it) + if (it.value() > count) { count = it.value(); best = it.key(); } + return best; + }; + auto window = [](const QPalette &p) { return p.color(QPalette::Active, QPalette::Window).rgb(); }; + + for (const QPalette &palette : {QET::Palette::fusionDark(), QET::Palette::fusionLight()}) + { + QApplication::setPalette(palette); + QTest::qWait(50); + QCOMPARE(background(plain), window(palette)); + // Qt leaves the style-sheet widget behind; this is the defect. + QVERIFY2(background(button) != window(palette), "Qt now updates style-sheet widgets itself; refreshStyleSheets() is redundant"); + + QET::Palette::refreshStyleSheets(); + QTest::qWait(50); + QCOMPARE(background(button), window(palette)); + } +} + int main(int argc, char **argv) { // Widgets are painted for real, on Qt's offscreen platform so the test