Compare commits

...

9 Commits

Author SHA1 Message Date
Laurent Trinques e7e2864641 Merge pull request #889 from ispyisail/fix/802-reload-element-drawings
Add "Reload element drawings" to refresh placed elements
2026-09-16 19:52:12 +02:00
Laurent Trinques 6562498ca2 Snap update for qml QJSEngine 2026-09-16 15:27:49 +02:00
Laurent Trinques 38e504fb7e CI windows-build add mingw-w64-ucrt-x86_64-qt6-declarative for QJSEngine 2026-09-16 15:22:13 +02:00
Laurent Trinques 4aef4d17d2 git submodule update --remote elements 2026-09-16 14:58:55 +02:00
Laurent Trinques c89243a36f Merge pull request #894 from jp2images/fix-macos-fusion-dark-palette
Give Fusion a palette it can draw with on macOS
2026-09-16 14:21:13 +02:00
Jeff Patterson b208a4f131 Show the text color as a swatch and keep the terminal plan preview on white
The Selection properties "texts" tab painted the color value's own text in
that color, so the default black was unreadable on a dark palette. Show a
swatch in the cell instead and leave the text in the palette's color.

The terminal plan preview draws black ink with a white brush, like the
printed page it previews, on whatever background the view inherits from
the palette. Give the view a white background.
2026-09-16 04:39:31 -05:00
Jeff Patterson 1c8e7ed052 Give Fusion a palette it can draw with on macOS
main.cpp has forced the Fusion style on macOS since 2019, but the palette
still came from Qt's macOS platform theme, which is built for the native
style. It hands Fusion a Window, Button and Base that are the same color,
a Dark lighter than Light, and in dark mode an Inactive ButtonText of
black. Fusion derives its frames, gradients and indicators from those
roles, so fields had no edges, the radio buttons in the text alignment
dialog vanished, and the "Handles" combo box in the diagram editor
toolbar drew its text black on a dark combo the moment the window lost
focus. Light mode had the same flatness, with Window, Button and Base
all white.

Add QET::Palette (sources/qetpalette.{h,cpp}) with a light and a dark
palette laid out the way Fusion expects, and install one from
QETApp::initStyle() on macOS when the running style is Fusion, choosing
by the platform palette's lightness and keeping the platform's accent
color when it reads at 4.5:1. On macOS the base palette is now applied
whether or not "use system colors" is checked, since the system palette
cannot be drawn by Fusion; that setting only decides whether style.css
is layered on top (#467). On Qt 6.5+ the app follows the OS light/dark
switch through QStyleHints::colorSchemeChanged.

Other platforms are untouched: Fusion is Qt's default style on Linux
desktops without a platform theme, and the palette there carries the
user's desktop colors. Making Fusion and this palette the default
everywhere is discussed in #870.

tests/qttest/tst_qetpalette checks every text role pair at WCAG 4.5:1
(3:1 disabled), that Inactive equals Active, and paints a Fusion combo
box, radio buttons, buttons and a line edit on the offscreen platform to
measure the ink against its background. Set QET_TEST_DUMP_DIR to keep
the rendered images.
2026-09-16 04:39:31 -05:00
Laurent Trinques 9004db1d99 Reload element drawings: keep old drawing on failure, skip elements whose geometry changed (#802)
- Element::reloadPicture() now returns a ReloadPictureResult and never
  clears the current drawing before a successful rebuild: a missing or
  unreadable definition leaves the element as it was instead of blank.
- Elements whose size, hotspot or terminals (added, removed or moved)
  differ from the new definition are not redrawn: the new drawing would
  no longer match their bounding rect and live terminals.
- The action lists those elements and warns that they must be removed
  and re-inserted, which deletes the conductors already connected to
  them.
- Status tip states the action is not undoable.
2026-09-16 21:35:17 +12:00
ispyisail 43d27a9563 Add "Reload element drawings" to refresh placed elements (#802)
A placed element is drawn once from its definition at construction --
buildFromXml() only turns terminal/input/dynamic_text tags into live
child objects, every other primitive (line, rect, ellipse, polygon,
arc, text) is pre-rendered into a QPicture by ElementPictureFactory,
cached forever under the element's uuid with no invalidation path
anywhere in the codebase. Edit and save a symbol's drawing and every
already-placed instance keeps showing the old one until the project
is closed and reopened.

Fix, scoped to what is safe to do without ever risking a conductor or
a dynamic text's per-instance state:

- ElementPictureFactory::dropCache(location) forgets the cached
  drawing for one location, so the next fetch rebuilds it from the
  definition's current content.
- Element::reloadPicture() re-fetches and repaints one instance.
- Projet > "Recharger les dessins des éléments": walks every diagram,
  drops each distinct location's cache once, then reloads every placed
  instance.

Deliberately does not touch terminals or dynamic texts -- a definition
whose terminal positions moved still needs the existing remove-and-
reinsert workflow, since terminals are what conductors are attached to
and a wrong guess there would silently misconnect wires.

Verified: build clean, ctest 6/6. Triggered the new action on a real,
densely-wired project (76 elements) via exact keyboard-menu navigation
cross-checked against the menu's own addAction order -- ran to
completion, correct confirmation dialog, no crash, diagram unchanged
and uncorrupted afterward. Could not complete a live edit-and-watch-
it-update trace: opening the element editor on a selected item via
GUI automation was unreliable in this environment (same class of
friction as PR #888), and this sandbox has no file-based (common://)
element to mutate on disk as a shortcut -- every example project
embeds its elements. The mechanism itself is traced correct:
ElementsLocation::xml() for an embed:// location reads the project's
live in-memory collection DOM on every call, so a dropped cache
rebuilds from whatever was most recently saved.

Refs #802 (own analysis comment, 2026-08-31).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 16:24:44 +12:00
17 changed files with 967 additions and 7 deletions
+1
View File
@@ -56,6 +56,7 @@ jobs:
mingw-w64-ucrt-x86_64-extra-cmake-modules
mingw-w64-ucrt-x86_64-nsis
mingw-w64-ucrt-x86_64-angleproject
mingw-w64-ucrt-x86_64-qt6-declarative
- name: Cache ccache
uses: actions/cache@v5
+2
View File
@@ -73,6 +73,7 @@ parts:
- git
- sqlite3
- xdg-user-dirs
- libqt6qml6
# libxcb-cursor0 workaround was needed against the Qt5/KF5 core22 content
# snap (issue #373). Not yet re-verified against kf6-core24 — remove this
# if the Qt6 xcb platform plugin loads cleanly without it.
@@ -84,6 +85,7 @@ parts:
- libsqlite3-dev
- qt6-tools-dev
- qt6-base-private-dev
- qt6-declarative-dev
- pkgconf
# Qt6 PrintSupport records Cups::Cups as a third-party dependency
# (qprint_p.h includes <cups/ppd.h>), so find_package(Qt6 PrintSupport)
+2
View File
@@ -249,6 +249,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/qet.h
${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/qeticons.h
${QET_DIR}/sources/qetpalette.cpp
${QET_DIR}/sources/qetpalette.h
${QET_DIR}/sources/qetinformation.cpp
${QET_DIR}/sources/qetinformation.h
${QET_DIR}/sources/qetmainwindow.cpp
@@ -29,6 +29,9 @@ TerminalStripLayoutEditor::TerminalStripLayoutEditor(QSharedPointer<TerminalStri
{
ui->setupUi(this);
ui->m_graphics_view->setScene(new QGraphicsScene{ this });
// The preview is printed output: black ink on a white page, whatever
// the application palette.
ui->m_graphics_view->setBackgroundBrush(Qt::white);
ui->m_graphics_view->scene()->addItem(&m_preview_strip_item);
updateUi();
}
+23
View File
@@ -94,6 +94,29 @@ void ElementPictureFactory::getPictures(const ElementsLocation &location, QPictu
}
}
/**
@brief ElementPictureFactory::dropCache
Forget the cached drawing of the element at @p location, so the next
getPictures()/pixmap()/getPrimitives() call rebuilds it from the
definition's current content instead of returning what was cached the
first time this location was drawn.
A placed Element keeps its own copy of the picture in m_picture /
m_low_zoom_picture (set once, in buildFromXml()), so dropping the shared
cache here does not by itself change what is on screen -- callers doing
a manual refresh (bugtracker #802) still need each Element to re-fetch
its picture afterwards.
@param location
*/
void ElementPictureFactory::dropCache(const ElementsLocation &location)
{
const QUuid uuid = cacheKey(location);
m_pictures_H.remove(uuid);
m_low_pictures_H.remove(uuid);
m_pixmap_H.remove(uuid);
m_primitives_H.remove(uuid);
}
/**
@brief ElementPictureFactory::pixmap
@param location
+1
View File
@@ -85,6 +85,7 @@ class ElementPictureFactory
void getPictures(const ElementsLocation &location, QPicture &picture, QPicture &low_picture);
QPixmap pixmap(const ElementsLocation &location);
ElementPictureFactory::primitives getPrimitives(const ElementsLocation &location);
void dropCache(const ElementsLocation &location);
private:
ElementPictureFactory() {}
+40 -1
View File
@@ -26,6 +26,7 @@
#include "projectview.h"
#include "qetdiagrameditor.h"
#include "qeticons.h"
#include "qetpalette.h"
#include "utils/qetutils.h"
#include "qetmessagebox.h"
#include "qetproject.h"
@@ -50,6 +51,7 @@
#include <QFontDatabase>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QStyleHints>
#ifdef BUILD_WITHOUT_KF
# include "ui/nokde/kautosavefile.h"
#else
@@ -1786,8 +1788,10 @@ void QETApp::invertMainWindowVisibility(QWidget *window) {
false pour utiliser celles du theme en cours
*/
void QETApp::useSystemPalette(bool use) {
if (use) {
// The base palette is always initial_palette_ (see initStyle()); the
// setting only decides whether the user's style.css is layered on top.
qApp->setPalette(initial_palette_);
if (use) {
// Drop any stylesheet previously loaded from style.css: with system
// colors requested, the palette set just above is what provides them.
//
@@ -2341,9 +2345,44 @@ void QETApp::initStyle()
{
initial_palette_ = qApp->palette();
#ifdef Q_OS_MACOS
// main.cpp forces the Fusion style on macOS, but the palette Qt hands
// us there is the one its platform theme builds for the native style:
// Window, Button and Base share one color, and in dark mode the
// Inactive ButtonText is black. Fusion draws its frames, gradients and
// combo box text from those roles, so controls lose their edges and
// combo text goes black once the window loses focus. Replace it with a
// palette laid out the way Fusion expects (see qetpalette.h).
//
// macOS only: on Linux, Fusion is Qt's default style on desktops
// without a platform theme, and the palette there carries the user's
// desktop colors, which must stay in effect. Making Fusion and this
// palette the default everywhere is discussed in #870.
if (QET::Palette::styleIsFusion(qApp->style()))
initial_palette_ = QET::Palette::forFusion(initial_palette_);
#endif
//Apply or not the system style
QSettings settings;
useSystemPalette(settings.value("usesystemcolors", true).toBool());
#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
// Setting an application palette stops Qt from following the OS
// light/dark switch on its own, so follow it here. The platform accent
// color is not reachable any more at this point; the palette's own
// selection blue is used instead.
connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this,
[this](Qt::ColorScheme scheme)
{
if (!QET::Palette::styleIsFusion(qApp->style()))
return;
initial_palette_ = scheme == Qt::ColorScheme::Dark
? QET::Palette::fusionDark()
: QET::Palette::fusionLight();
QSettings settings;
useSystemPalette(settings.value("usesystemcolors", true).toBool());
});
#endif
}
/**
+105
View File
@@ -22,6 +22,7 @@
#include "autoNum/ui/autonumberingdockwidget.h"
#include "conductornumexport.h"
#include "diagramcommands.h"
#include "diagramcontent.h"
#include "diagramevent/diagrameventaddimage.h"
#ifdef QET_HAS_QTPDF
#include "diagramevent/diagrameventaddpdf.h"
@@ -32,6 +33,7 @@
#include "diagramevent/diagrameventaddpaste.h"
#include "diagramview.h"
#include "elementspanelwidget.h"
#include "factory/elementpicturefactory.h"
#include "factory/qetgraphicstablefactory.h"
#include "print/projectprintwindow.h"
#include "project/projectpropertieshandler.h"
@@ -549,6 +551,13 @@ void QETDiagramEditor::setUpActions()
m_terminal_numbering = new QAction(QET::Icons::TerminalStrip, tr("Numérotation automatique des bornes"), this);
connect(m_terminal_numbering, &QAction::triggered, this, &QETDiagramEditor::slot_terminalNumbering);
// Reload element drawings from their current definition (bugtracker #802)
m_reload_element_drawings = new QAction(QET::Icons::ViewRefresh, tr("Recharger les dessins des éléments"), this);
m_reload_element_drawings->setStatusTip(
tr("Redessine chaque élément placé d'après sa définition actuelle,"
" sans avoir à fermer et rouvrir le projet (action non annulable)"));
connect(m_reload_element_drawings, &QAction::triggered, this, &QETDiagramEditor::slot_reloadElementDrawings);
#ifdef QET_EXPORT_PROJECT_DB
m_export_project_db = new QAction(QET::Icons::DocumentSpreadsheet, tr("Exporter la base de donnée interne du projet"), this);
connect(m_export_project_db, &QAction::triggered, [this]() {
@@ -1002,6 +1011,7 @@ void QETDiagramEditor::setUpMenu()
menu_project -> addAction(m_project_export_wiring_list);
menu_project -> addAction(m_project_wiring_list_view);
menu_project -> addAction(m_terminal_numbering);
menu_project -> addAction(m_reload_element_drawings);
#ifdef QET_EXPORT_PROJECT_DB
menu_project -> addSeparator();
menu_project -> addAction(m_export_project_db);
@@ -1856,6 +1866,7 @@ void QETDiagramEditor::slot_updateActions()
m_project_export_wiring_list -> setEnabled(opened_project);
m_project_wiring_list_view -> setEnabled(opened_project);
m_terminal_numbering -> setEnabled(editable_project);
m_reload_element_drawings -> setEnabled(opened_project);
#ifdef QET_EXPORT_PROJECT_DB
m_export_project_db -> setEnabled(editable_project);
#endif
@@ -2970,3 +2981,97 @@ void QETDiagramEditor::slot_terminalNumbering() {
}
}
}
/**
@brief QETDiagramEditor::slot_reloadElementDrawings
Redraw every placed element of the current project from its current
definition (bugtracker #802): a symbol edited and saved after being
placed keeps showing its old drawing otherwise, until the whole project
is closed and reopened.
Purely visual and not undoable, the way pressing a "refresh" button
would be: nothing is pushed on the undo stack and the project is not
marked as modified. Elements whose size, hotspot or terminals changed
are skipped and listed: they must be removed and re-inserted, which
deletes the conductors already connected to them.
*/
void QETDiagramEditor::slot_reloadElementDrawings() {
QETProject *project = currentProject();
if (!project) return;
QList<Element *> elements;
QSet<QString> dropped_locations;
for (Diagram *diagram : project->diagrams())
{
DiagramContent content(diagram, false);
for (Element *elmt : content.m_elements)
{
elements << elmt;
const QString key = elmt->location().toString();
if (!dropped_locations.contains(key))
{
ElementPictureFactory::instance()->dropCache(elmt->location());
dropped_locations.insert(key);
}
}
}
int reloaded = 0;
int unavailable = 0;
QStringList geometry_changed;
for (Element *elmt : elements)
{
switch (elmt->reloadPicture())
{
case Element::ReloadPictureResult::Reloaded:
++reloaded;
break;
case Element::ReloadPictureResult::Unavailable:
++unavailable;
break;
case Element::ReloadPictureResult::GeometryChanged:
{
const Diagram *diagram = elmt->diagram();
const QString folio = diagram
? tr("folio %1").arg(project->folioIndex(diagram) + 1)
: QString();
geometry_changed << QStringLiteral("%1 (%2)").arg(elmt->name(), folio);
break;
}
}
}
QString message = tr("%n élément(s) redessiné(s).", "", reloaded);
if (unavailable) {
message += QStringLiteral("\n\n")
% tr("%n élément(s) dont la définition est introuvable ou illisible :"
" leur dessin actuel a été conservé.", "", unavailable);
}
if (geometry_changed.isEmpty())
{
QET::QetMessageBox::information(
this, tr("Recharger les dessins des éléments"), message);
return;
}
message += QStringLiteral("\n\n")
% tr("%n élément(s) non redessiné(s) : leur taille, leur point de saisie"
" ou leurs bornes ont changé (borne ajoutée, supprimée ou déplacée).",
"", geometry_changed.size())
% QStringLiteral("\n\n")
% tr("Pour les mettre à jour, il faut les supprimer puis les réinsérer."
" Attention : cette opération supprime les conducteurs déjà reliés"
" à ces éléments, qu'il faudra retracer.");
//The full list goes in the expandable, scrollable details area
//so the dialog stays readable on large projects.
QMessageBox box(QMessageBox::Warning,
tr("Recharger les dessins des éléments"),
message,
QMessageBox::Ok,
this);
box.setDetailedText(geometry_changed.join(QLatin1Char('\n')));
box.exec();
}
+2
View File
@@ -136,6 +136,7 @@ class QETDiagramEditor : public QETMainWindow
void editProjectProperties(ProjectView *);
void editProjectProperties(QETProject *);
void slot_terminalNumbering();
void slot_reloadElementDrawings();
void editDiagramProperties(DiagramView *);
void editDiagramProperties(Diagram *);
void addDiagramToProject(QETProject *);
@@ -211,6 +212,7 @@ class QETDiagramEditor : public QETMainWindow
*m_project_export_wiring_list, ///< Action to export the wiring list
*m_project_wiring_list_view, ///< Action to show the wiring list read from the project database
*m_terminal_numbering, ///< Action to launch terminal numbering
*m_reload_element_drawings, ///< Action to redraw every placed element from its current definition
*m_export_project_db, ///Export to file the internal database of the current project
*m_tile_window, ///< Show MDI subwindows as tile
*m_cascade_window, ///< Show MDI subwindows as cascade
+133
View File
@@ -1784,6 +1784,139 @@ ElementsLocation Element::location() const
return m_location;
}
/**
@brief Element::reloadPicture
Re-fetch this element's drawing from its location and repaint.
A placed element is drawn once from its definition, at construction
(buildFromXml()), and nothing afterwards ever makes it look again --
editing and saving the definition leaves every already-placed instance
showing the old drawing until the project is closed and reopened
(bugtracker #802). This is the per-instance half of the fix.
Deliberately limited to the drawing. Terminals are what conductors are
attached to: if the new definition adds, removes or moves a terminal,
or changes the element size or hotspot, the new drawing would no longer
match the live terminals and bounding rect. Such an element is left
untouched and GeometryChanged is returned; it has to be removed and
re-inserted, which deletes the conductors already connected to it.
If the definition cannot be found or read, the current drawing is kept
and Unavailable is returned, so the element never goes blank.
Purely visual: nothing is pushed on the undo stack and the project is
not marked as modified.
@return what happened to this element
*/
Element::ReloadPictureResult Element::reloadPicture()
{
if (!m_location.exist()) {
return ReloadPictureResult::Unavailable;
}
const QDomElement definition = m_location.xml();
if (definition.isNull()) {
return ReloadPictureResult::Unavailable;
}
if (!definitionGeometryMatches(definition)) {
return ReloadPictureResult::GeometryChanged;
}
QPicture picture;
QPicture low_zoom_picture;
ElementPictureFactory::instance()->getPictures(m_location,
picture,
low_zoom_picture);
if (picture.isNull()) {
return ReloadPictureResult::Unavailable;
}
m_picture = picture;
m_low_zoom_picture = low_zoom_picture;
update();
return ReloadPictureResult::Reloaded;
}
/**
@brief Element::definitionGeometryMatches
Compare the geometry described by @p definition with this live element:
size and hotspot (normalized the same way setSize()/setHotspot() do it)
and the set of terminal positions (same parsing rules as
TerminalData::fromXml()).
@param definition : the <definition> root of the element
@return true if the new drawing can be applied without desynchronizing
the bounding rect or the terminals
*/
bool Element::definitionGeometryMatches(const QDomElement &definition) const
{
int w = 0, h = 0, hot_x = 0, hot_y = 0;
if (!QET::attributeIsAnInteger(definition, QStringLiteral("width"), &w) ||
!QET::attributeIsAnInteger(definition, QStringLiteral("height"), &h) ||
!QET::attributeIsAnInteger(definition, QStringLiteral("hotspot_x"), &hot_x) ||
!QET::attributeIsAnInteger(definition, QStringLiteral("hotspot_y"), &hot_y)) {
return false;
}
//Same rounding as setSize()
while (w % 10) ++w;
while (h % 10) ++h;
if (QSize(w, h) != dimensions) {
return false;
}
//Same clamping as setHotspot()
const QPoint new_hotspot = dimensions.isNull()
? QPoint(0, 0)
: QPoint(qMin(hot_x, w), qMin(hot_y, h));
if (new_hotspot != hotspot_coord) {
return false;
}
//Terminal positions described by the new definition
QList<QPointF> new_terminals;
for (QDomElement description = definition.firstChildElement(QStringLiteral("description")) ;
!description.isNull() ;
description = description.nextSiblingElement(QStringLiteral("description")))
{
for (QDomElement terminal = description.firstChildElement(QStringLiteral("terminal")) ;
!terminal.isNull() ;
terminal = terminal.nextSiblingElement(QStringLiteral("terminal")))
{
qreal x = 0.0, y = 0.0;
if (QET::attributeIsAReal(terminal, QStringLiteral("x"), &x) &&
QET::attributeIsAReal(terminal, QStringLiteral("y"), &y)) {
new_terminals << QPointF(x, y);
}
}
}
if (new_terminals.size() != m_terminals.size()) {
return false;
}
//Every live terminal must still exist at the same place
for (const Terminal *terminal : m_terminals)
{
const QPointF live_pos = mapFromScene(terminal->dockConductor());
bool found = false;
for (int i = 0 ; i < new_terminals.size() ; ++i)
{
const QPointF delta = new_terminals.at(i) - live_pos;
if (qAbs(delta.x()) < 0.01 && qAbs(delta.y()) < 0.01) {
new_terminals.removeAt(i);
found = true;
break;
}
}
if (!found) {
return false;
}
}
return true;
}
/**
* @brief Element::updateConductorTexts
*Slot that is triggered when a cable is *
+8
View File
@@ -83,6 +83,7 @@ class Element : public QetGraphicsItem
Element::kind link_type = Element::Simple);
~Element() override;
private:
bool definitionGeometryMatches(const QDomElement &definition) const;
Element(const Element &);
// attributes
@@ -153,6 +154,13 @@ class Element : public QetGraphicsItem
QString name() const override;
ElementsLocation location() const;
/// Result of Element::reloadPicture()
enum class ReloadPictureResult {
Reloaded, ///< drawing replaced by the current definition
Unavailable, ///< definition missing or unreadable, old drawing kept
GeometryChanged ///< size, hotspot or terminals changed, old drawing kept
};
ReloadPictureResult reloadPicture();
virtual void setHighlighted(bool);
void displayHelpLine(bool b = true);
QSize size() const;
+179
View File
@@ -0,0 +1,179 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "qetpalette.h"
#include <QStyle>
#include <cmath>
namespace {
/**
Set one role for the Active and Inactive groups (same color: Fusion
does not draw an inactive window differently, and a differing
Inactive color is what turned combo box text black on macOS) and
for the Disabled group.
*/
void setRole(QPalette &p, QPalette::ColorRole role,
const QColor &active, const QColor &disabled)
{
p.setColor(QPalette::Active, role, active);
p.setColor(QPalette::Inactive, role, active);
p.setColor(QPalette::Disabled, role, disabled);
}
/// Same color in all three groups.
void setRole(QPalette &p, QPalette::ColorRole role, const QColor &c)
{
setRole(p, role, c, c);
}
/// sRGB channel to linear light, as defined by WCAG 2.
double linear(int channel)
{
const double c = channel / 255.0;
return c <= 0.03928 ? c / 12.92 : std::pow((c + 0.055) / 1.055, 2.4);
}
double relativeLuminance(const QColor &c)
{
return 0.2126 * linear(c.red())
+ 0.7152 * linear(c.green())
+ 0.0722 * linear(c.blue());
}
/**
The selection color shared by both palettes: a blue that reads at
better than 6:1 under white text. Fusion's own default (48,140,198)
only reaches 3.7:1.
*/
const QColor kHighlight(30, 96, 176);
}
bool QET::Palette::styleIsFusion(const QStyle *style)
{
return style && style->objectName().compare(QLatin1String("fusion"),
Qt::CaseInsensitive) == 0;
}
bool QET::Palette::isDark(const QPalette &palette)
{
return palette.color(QPalette::Active, QPalette::Window).lightness() < 128;
}
double QET::Palette::contrastRatio(const QColor &a, const QColor &b)
{
double lighter = relativeLuminance(a);
double darker = relativeLuminance(b);
if (lighter < darker)
std::swap(lighter, darker);
return (lighter + 0.05) / (darker + 0.05);
}
QPalette QET::Palette::fusionLight()
{
QPalette p;
const QColor window(239, 239, 239);
const QColor text(Qt::black);
const QColor disabled_text(128, 128, 128);
setRole(p, QPalette::Window, window);
setRole(p, QPalette::WindowText, text, disabled_text);
setRole(p, QPalette::Base, Qt::white, window);
setRole(p, QPalette::AlternateBase, QColor(247, 247, 247));
setRole(p, QPalette::Text, text, disabled_text);
setRole(p, QPalette::Button, window);
setRole(p, QPalette::ButtonText, text, disabled_text);
setRole(p, QPalette::BrightText, Qt::white);
setRole(p, QPalette::Highlight, kHighlight, QColor(145, 145, 145));
setRole(p, QPalette::HighlightedText, Qt::white);
setRole(p, QPalette::ToolTipBase, QColor(255, 255, 220));
setRole(p, QPalette::ToolTipText, text);
setRole(p, QPalette::Link, QColor(0, 90, 170));
setRole(p, QPalette::LinkVisited, QColor(110, 40, 140));
setRole(p, QPalette::PlaceholderText, QColor(118, 118, 118));
// The 3D roles Fusion shades frames and gradients with.
setRole(p, QPalette::Light, Qt::white);
setRole(p, QPalette::Midlight, QColor(202, 202, 202));
setRole(p, QPalette::Mid, QColor(184, 184, 184));
setRole(p, QPalette::Dark, QColor(159, 159, 159));
setRole(p, QPalette::Shadow, QColor(118, 118, 118));
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
setRole(p, QPalette::Accent, kHighlight);
#endif
return p;
}
QPalette QET::Palette::fusionDark()
{
QPalette p;
const QColor window(53, 53, 53);
const QColor text(220, 220, 220);
// Fusion lifts a button's fill to about (89,89,89) at the top of its
// gradient; 175 is the darkest grey that still reads 3:1 on that.
const QColor disabled_text(175, 175, 175);
setRole(p, QPalette::Window, window);
setRole(p, QPalette::WindowText, text, disabled_text);
setRole(p, QPalette::Base, QColor(30, 30, 30), QColor(45, 45, 45));
setRole(p, QPalette::AlternateBase, QColor(45, 45, 45));
setRole(p, QPalette::Text, text, disabled_text);
setRole(p, QPalette::Button, QColor(66, 66, 66));
setRole(p, QPalette::ButtonText, text, disabled_text);
setRole(p, QPalette::BrightText, QColor(255, 90, 90));
setRole(p, QPalette::Highlight, kHighlight, QColor(80, 80, 80));
setRole(p, QPalette::HighlightedText, Qt::white);
setRole(p, QPalette::ToolTipBase, QColor(66, 66, 66));
setRole(p, QPalette::ToolTipText, text);
setRole(p, QPalette::Link, QColor(100, 170, 255));
setRole(p, QPalette::LinkVisited, QColor(190, 130, 255));
setRole(p, QPalette::PlaceholderText, QColor(150, 150, 150));
setRole(p, QPalette::Light, QColor(90, 90, 90));
setRole(p, QPalette::Midlight, QColor(75, 75, 75));
setRole(p, QPalette::Mid, QColor(35, 35, 35));
setRole(p, QPalette::Dark, QColor(20, 20, 20));
setRole(p, QPalette::Shadow, QColor(10, 10, 10));
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
setRole(p, QPalette::Accent, kHighlight);
#endif
return p;
}
QPalette QET::Palette::withPlatformAccent(QPalette palette, const QPalette &platform)
{
const QColor highlight = platform.color(QPalette::Active, QPalette::Highlight);
const QColor highlighted_text = platform.color(QPalette::Active, QPalette::HighlightedText);
if (!highlight.isValid() || !highlighted_text.isValid())
return palette;
if (contrastRatio(highlight, highlighted_text) < 4.5)
return palette;
palette.setColor(QPalette::Active, QPalette::Highlight, highlight);
palette.setColor(QPalette::Inactive, QPalette::Highlight, highlight);
palette.setColor(QPalette::Active, QPalette::HighlightedText, highlighted_text);
palette.setColor(QPalette::Inactive, QPalette::HighlightedText, highlighted_text);
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
palette.setColor(QPalette::Active, QPalette::Accent, highlight);
palette.setColor(QPalette::Inactive, QPalette::Accent, highlight);
#endif
return palette;
}
QPalette QET::Palette::forFusion(const QPalette &platform)
{
return withPlatformAccent(isDark(platform) ? fusionDark() : fusionLight(),
platform);
}
+93
View File
@@ -0,0 +1,93 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#ifndef QET_PALETTE_H
#define QET_PALETTE_H
#include <QPalette>
class QStyle;
/**
Palettes tuned for the Fusion style.
main.cpp forces the Fusion style on macOS, but the palette still comes
from Qt's macOS platform theme, which is built for the native style.
That palette gives Window, Button and Base the same color, makes Dark
lighter than Light, and in dark mode sets the Inactive ButtonText to
black. Fusion derives its outlines, gradients and indicator colors from
those roles, so under that palette controls lose their edges, radio
indicators vanish, and combo box text turns black as soon as the window
loses focus.
This namespace provides a light and a dark palette with the role
separation Fusion expects, and the WCAG 2 contrast helper the tests use
to keep them readable.
*/
namespace QET {
namespace Palette {
/**
@return true when the style draws with Fusion's rules,
i.e. its object name is "fusion".
*/
bool styleIsFusion(const QStyle *style);
/**
@return true when the palette's Window color is dark
(lightness below 128).
*/
bool isDark(const QPalette &palette);
/**
WCAG 2 contrast ratio between two opaque colors, from 1 (equal)
to 21 (black on white). Normal text needs at least 4.5, large
text and non-text controls at least 3.
*/
double contrastRatio(const QColor &a, const QColor &b);
/**
A light palette with the role separation Fusion expects: Window
and Button a light grey, Base white, Text black, and Inactive
equal to Active so nothing changes when the window loses focus.
*/
QPalette fusionLight();
/**
A dark palette with the same role separation: Window a mid
grey, Button lighter than Window, Base darker than Window, Text
light, and Inactive equal to Active.
*/
QPalette fusionDark();
/**
Copy the Highlight and HighlightedText colors of the platform
palette into ours, so the user's accent color survives, but only
when the pair reads at 4.5:1 or better. Otherwise ours is kept.
*/
QPalette withPlatformAccent(QPalette palette, const QPalette &platform);
/**
The palette QET should use under Fusion, given the palette the
platform handed to the application: fusionDark() when the
platform palette is dark, fusionLight() otherwise, with the
platform's accent color kept when it is readable.
*/
QPalette forFusion(const QPalette &platform);
}
}
#endif
+5 -3
View File
@@ -251,8 +251,10 @@ QList<QStandardItem *> DynamicElementTextModel::itemsForText(
QStandardItem *color = new QStandardItem(tr("Couleur"));
color->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
// Shown as a swatch next to the value, not as the text color: black
// text (the default) is unreadable on a dark palette.
QStandardItem *colora = new QStandardItem;
colora->setData(deti->color(), Qt::ForegroundRole);
colora->setData(deti->color(), Qt::DecorationRole);
colora->setData(deti->color(), Qt::EditRole);
colora->setData(DynamicElementTextModel::color, Qt::UserRole+1);
colora->setFlags(Qt::ItemIsSelectable
@@ -1500,7 +1502,7 @@ void DynamicElementTextModel::updateDataFromText(DynamicElementTextItem *deti,
case color:
{
qsi->child(color_txt_row,1)->setData(deti->color(), Qt::EditRole);
qsi->child(color_txt_row,1)->setData(deti->color(), Qt::ForegroundRole);
qsi->child(color_txt_row,1)->setData(deti->color(), Qt::DecorationRole);
break;
}
case pos:
@@ -1832,7 +1834,7 @@ void DynamicTextItemDelegate::setModelData(
{
QColor color = editor->property("color").value<QColor>();
qsi->setData(color, Qt::EditRole);
qsi->setData(color, Qt::ForegroundRole);
qsi->setData(color, Qt::DecorationRole);
}
return;
}
+9
View File
@@ -92,6 +92,15 @@ add_test(NAME tst_contactusage COMMAND tst_contactusage)
target_include_directories(tst_contactusage PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_contactusage PRIVATE Qt::Test)
add_executable(
tst_qetpalette
tst_qetpalette.cpp
${QET_DIR}/sources/qetpalette.cpp
${QET_DIR}/sources/qetpalette.h)
add_test(NAME tst_qetpalette COMMAND tst_qetpalette)
target_include_directories(tst_qetpalette PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_qetpalette PRIVATE Qt::Test Qt::Widgets)
add_executable(
tst_smart_device
tst_smart_device.cpp
+358
View File
@@ -0,0 +1,358 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include <QtTest>
#include <QApplication>
#include <QCheckBox>
#include <QComboBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMainWindow>
#include <QPushButton>
#include <QRadioButton>
#include <QStyleFactory>
#include <QToolBar>
#include "qetpalette.h"
using QET::Palette::contrastRatio;
/**
Contrast checks for the palettes QET installs under the Fusion style.
Two levels. The role tests check the numbers directly: every
foreground/background pair Fusion draws text with must read at 4.5:1
(WCAG AA for normal text), 3:1 when disabled. The render tests paint
real widgets with Fusion on the offscreen platform and measure the ink
against its background, which is what a user sees and what broke on
macOS: the combo box text went black on a dark combo as soon as the
window lost focus, and radio indicators disappeared into the window.
*/
class tst_qetpalette : public QObject
{
Q_OBJECT
private slots:
void textRolesAreReadable_data();
void textRolesAreReadable();
void controlsStandOutFromWindow_data();
void controlsStandOutFromWindow();
void inactiveEqualsActive_data();
void inactiveEqualsActive();
void platformAccentKeptOnlyWhenReadable();
void forFusionPicksVariantByLightness();
void styleIsFusionMatchesObjectName();
void renderedWidgetsAreReadable_data();
void renderedWidgetsAreReadable();
private:
static void addPaletteRows();
};
namespace {
const double kTextRatio = 4.5; ///< WCAG AA, normal text
const double kDisabledRatio = 3.0; ///< disabled text
/**
An unchecked Fusion radio button is a disc filled with Base and
outlined with Window darkened by 40%: on a dark palette only the
fill is left to see it by, and 3:1 is out of reach for any Fusion
dark theme. The dark palette here reaches 1.36 (light: 2.4); the
macOS platform palette gave 1.06, which is the invisible case.
*/
const double kIndicatorRatio = 1.3;
struct TextPair {
const char *name;
QPalette::ColorRole fg;
QPalette::ColorRole bg;
};
const TextPair kTextPairs[] = {
{"WindowText/Window", QPalette::WindowText, QPalette::Window},
{"Text/Base", QPalette::Text, QPalette::Base},
{"Text/AlternateBase", QPalette::Text, QPalette::AlternateBase},
{"ButtonText/Button", QPalette::ButtonText, QPalette::Button},
{"ButtonText/Window", QPalette::ButtonText, QPalette::Window},
{"HighlightedText/Highlight", QPalette::HighlightedText, QPalette::Highlight},
{"ToolTipText/ToolTipBase", QPalette::ToolTipText, QPalette::ToolTipBase},
{"PlaceholderText/Base", QPalette::PlaceholderText, QPalette::Base},
{"Link/Base", QPalette::Link, QPalette::Base},
{"Link/Window", QPalette::Link, QPalette::Window},
};
/**
Grab a widget for measuring. With QET_TEST_DUMP_DIR set, the image
is also written there as <row>-<name>.png so a failure can be
looked at.
*/
QImage grab(QWidget *widget, const char *name)
{
const QImage image = widget->grab().toImage();
const QByteArray dir = qgetenv("QET_TEST_DUMP_DIR");
if (!dir.isEmpty())
image.save(QString("%1/%2-%3.png").arg(QString::fromLocal8Bit(dir),
QTest::currentDataTag(), name));
return image;
}
/**
Contrast between the background of a rendered widget (its most
frequent color) and the ink drawn on it (the pixel whose luminance
differs most from the background), inside rect.
*/
double inkContrast(const QImage &image, const QRect &rect)
{
QHash<QRgb, int> histogram;
for (int y = rect.top(); y <= rect.bottom(); ++y)
for (int x = rect.left(); x <= rect.right(); ++x)
++histogram[image.pixel(x, y)];
QRgb background = 0;
int best = -1;
for (auto it = histogram.cbegin(); it != histogram.cend(); ++it)
if (it.value() > best) { best = it.value(); background = it.key(); }
double contrast = 1.0;
for (auto it = histogram.cbegin(); it != histogram.cend(); ++it)
contrast = qMax(contrast, contrastRatio(QColor(background),
QColor(it.key())));
return contrast;
}
}
void tst_qetpalette::addPaletteRows()
{
QTest::addColumn<QPalette>("palette");
QTest::newRow("light") << QET::Palette::fusionLight();
QTest::newRow("dark") << QET::Palette::fusionDark();
}
void tst_qetpalette::textRolesAreReadable_data()
{
addPaletteRows();
}
void tst_qetpalette::textRolesAreReadable()
{
QFETCH(QPalette, palette);
for (const TextPair &pair : kTextPairs)
{
const double active = contrastRatio(
palette.color(QPalette::Active, pair.fg),
palette.color(QPalette::Active, pair.bg));
const double disabled = contrastRatio(
palette.color(QPalette::Disabled, pair.fg),
palette.color(QPalette::Disabled, pair.bg));
QVERIFY2(active >= kTextRatio,
qPrintable(QString("%1 active: %2 < %3")
.arg(pair.name).arg(active).arg(kTextRatio)));
QVERIFY2(disabled >= kDisabledRatio,
qPrintable(QString("%1 disabled: %2 < %3")
.arg(pair.name).arg(disabled).arg(kDisabledRatio)));
}
}
void tst_qetpalette::controlsStandOutFromWindow_data()
{
addPaletteRows();
}
/**
Fusion fills fields with Base and buttons with Button, and outlines
both with Window darkened by 40%. The macOS platform palette set Base,
Button and Window equal, so fields had no edge at all. In a dark
palette the darkened outline is nearly black on near-black and the
Button fill has to carry the separation on its own; in a light palette
the outline does that job, as in Fusion's own standard palette where
Button equals Window.
*/
void tst_qetpalette::controlsStandOutFromWindow()
{
QFETCH(QPalette, palette);
const QColor window = palette.color(QPalette::Active, QPalette::Window);
const QColor button = palette.color(QPalette::Active, QPalette::Button);
const QColor base = palette.color(QPalette::Active, QPalette::Base);
// White fields on Fusion's standard light grey (239) panel are 1.15:1;
// the macOS platform palette had them equal.
QVERIFY2(contrastRatio(window, base) >= 1.1, "Base blends into Window");
if (QET::Palette::isDark(palette))
QVERIFY2(contrastRatio(window, button) >= 1.1, "Button blends into Window");
else
QVERIFY2(contrastRatio(window, window.darker(140)) >= 1.5, "Fusion's outline blends into Window");
QVERIFY2(contrastRatio(palette.color(QPalette::Light), palette.color(QPalette::Dark)) >= 1.5,
"Light and Dark too close for Fusion's bevels");
QVERIFY2(palette.color(QPalette::Light).lightness() > palette.color(QPalette::Dark).lightness(),
"Light must be lighter than Dark");
}
void tst_qetpalette::inactiveEqualsActive_data()
{
addPaletteRows();
}
/**
The macOS platform palette sets Inactive ButtonText to black in dark
mode, because native inactive buttons are drawn light. Fusion never
draws them light, so combo box and button text vanished when the
window lost focus. Our palettes keep every role identical between the
two groups.
*/
void tst_qetpalette::inactiveEqualsActive()
{
QFETCH(QPalette, palette);
for (int role = 0; role < QPalette::NColorRoles; ++role)
{
const auto r = static_cast<QPalette::ColorRole>(role);
QCOMPARE(palette.color(QPalette::Inactive, r),
palette.color(QPalette::Active, r));
}
}
void tst_qetpalette::platformAccentKeptOnlyWhenReadable()
{
QPalette readable;
readable.setColor(QPalette::Active, QPalette::Highlight, QColor(74, 100, 66));
readable.setColor(QPalette::Active, QPalette::HighlightedText, Qt::white);
QPalette kept = QET::Palette::withPlatformAccent(QET::Palette::fusionDark(), readable);
QCOMPARE(kept.color(QPalette::Active, QPalette::Highlight), QColor(74, 100, 66));
QCOMPARE(kept.color(QPalette::Inactive, QPalette::Highlight), QColor(74, 100, 66));
QCOMPARE(kept.color(QPalette::Active, QPalette::HighlightedText), QColor(Qt::white));
QPalette faint;
faint.setColor(QPalette::Active, QPalette::Highlight, QColor(200, 200, 200));
faint.setColor(QPalette::Active, QPalette::HighlightedText, Qt::white);
QPalette ours = QET::Palette::fusionDark();
QPalette rejected = QET::Palette::withPlatformAccent(ours, faint);
QCOMPARE(rejected.color(QPalette::Active, QPalette::Highlight),
ours.color(QPalette::Active, QPalette::Highlight));
}
void tst_qetpalette::forFusionPicksVariantByLightness()
{
QPalette dark_platform;
dark_platform.setColor(QPalette::Active, QPalette::Window, QColor(30, 30, 30));
QVERIFY(QET::Palette::isDark(dark_platform));
QVERIFY(QET::Palette::isDark(QET::Palette::forFusion(dark_platform)));
QPalette light_platform;
light_platform.setColor(QPalette::Active, QPalette::Window, Qt::white);
QVERIFY(!QET::Palette::isDark(light_platform));
QVERIFY(!QET::Palette::isDark(QET::Palette::forFusion(light_platform)));
}
void tst_qetpalette::styleIsFusionMatchesObjectName()
{
QScopedPointer<QStyle> fusion(QStyleFactory::create("Fusion"));
QVERIFY(!fusion.isNull());
QVERIFY(QET::Palette::styleIsFusion(fusion.data()));
QScopedPointer<QStyle> windows(QStyleFactory::create("Windows"));
if (!windows.isNull())
QVERIFY(!QET::Palette::styleIsFusion(windows.data()));
QVERIFY(!QET::Palette::styleIsFusion(nullptr));
}
void tst_qetpalette::renderedWidgetsAreReadable_data()
{
addPaletteRows();
}
/**
Paint the widgets that broke on macOS with Fusion and measure the ink.
The combo box sits in a toolbar exactly like the "Handles" size
selector in the diagram editor; the radio buttons have no text, like
the ones in the text alignment dialog.
*/
void tst_qetpalette::renderedWidgetsAreReadable()
{
QFETCH(QPalette, palette);
QApplication::setStyle(QStyleFactory::create("Fusion"));
QApplication::setPalette(palette);
QMainWindow window;
QToolBar *toolbar = window.addToolBar("view");
auto *host = new QWidget;
auto *host_layout = new QHBoxLayout(host);
host_layout->addWidget(new QLabel("Handles :"));
auto *combo = new QComboBox;
combo->addItems({"x 1", "x 2", "x 3"});
host_layout->addWidget(combo);
toolbar->addWidget(host);
auto *central = new QWidget;
auto *layout = new QHBoxLayout(central);
auto *radio_off = new QRadioButton;
auto *radio_on = new QRadioButton;
radio_on->setChecked(true);
auto *check = new QCheckBox("check");
auto *button = new QPushButton("Button");
auto *disabled = new QPushButton("Disabled");
disabled->setEnabled(false);
auto *edit = new QLineEdit("field");
for (QWidget *w : {static_cast<QWidget *>(radio_off), static_cast<QWidget *>(radio_on),
static_cast<QWidget *>(check), static_cast<QWidget *>(button),
static_cast<QWidget *>(disabled), static_cast<QWidget *>(edit)})
layout->addWidget(w);
window.setCentralWidget(central);
window.resize(520, 140);
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
// Text area of the combo: skip the frame and the arrow on the right.
const QImage combo_image = grab(combo, "combo");
const QRect combo_text = combo->rect().adjusted(6, 4, -22, -4);
const double combo_contrast = inkContrast(combo_image, combo_text);
QVERIFY2(combo_contrast >= kTextRatio,
qPrintable(QString("combo box text: %1").arg(combo_contrast)));
// Radio indicators: the whole widget is the indicator, no text. The
// checked one has a dot to read by, the unchecked one only its disc.
const double radio_on_contrast = inkContrast(grab(radio_on, "radio-checked"), radio_on->rect());
QVERIFY2(radio_on_contrast >= kDisabledRatio,
qPrintable(QString("radio (checked): %1").arg(radio_on_contrast)));
const double radio_off_contrast = inkContrast(grab(radio_off, "radio-unchecked"), radio_off->rect());
QVERIFY2(radio_off_contrast >= kIndicatorRatio,
qPrintable(QString("radio (unchecked): %1").arg(radio_off_contrast)));
const QImage button_image = grab(button, "button");
const double button_contrast = inkContrast(button_image, button->rect().adjusted(6, 4, -6, -4));
QVERIFY2(button_contrast >= kTextRatio,
qPrintable(QString("push button text: %1").arg(button_contrast)));
const QImage disabled_image = grab(disabled, "button-disabled");
const double disabled_contrast = inkContrast(disabled_image, disabled->rect().adjusted(6, 4, -6, -4));
QVERIFY2(disabled_contrast >= kDisabledRatio,
qPrintable(QString("disabled button text: %1").arg(disabled_contrast)));
const QImage edit_image = grab(edit, "lineedit");
const double edit_contrast = inkContrast(edit_image, edit->rect().adjusted(4, 3, -4, -3));
QVERIFY2(edit_contrast >= kTextRatio,
qPrintable(QString("line edit text: %1").arg(edit_contrast)));
}
int main(int argc, char **argv)
{
// Widgets are painted for real, on Qt's offscreen platform so the test
// runs the same on a build box and on a desktop.
if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM"))
qputenv("QT_QPA_PLATFORM", "offscreen");
QApplication app(argc, argv);
tst_qetpalette test;
QTEST_SET_MAIN_SOURCE_PATH
return QTest::qExec(&test, argc, argv);
}
#include "tst_qetpalette.moc"