Merge remote-tracking branch 'upstream/master' into feature/162-js-scripting

# Conflicts:
#	sources/qetdiagrameditor.cpp
#	sources/qetdiagrameditor.h
This commit is contained in:
ispyisail
2026-09-17 06:50:35 +12:00
19 changed files with 991 additions and 22 deletions
+1
View File
@@ -56,6 +56,7 @@ jobs:
mingw-w64-ucrt-x86_64-extra-cmake-modules mingw-w64-ucrt-x86_64-extra-cmake-modules
mingw-w64-ucrt-x86_64-nsis mingw-w64-ucrt-x86_64-nsis
mingw-w64-ucrt-x86_64-angleproject mingw-w64-ucrt-x86_64-angleproject
mingw-w64-ucrt-x86_64-qt6-declarative
- name: Cache ccache - name: Cache ccache
uses: actions/cache@v5 uses: actions/cache@v5
+2
View File
@@ -73,6 +73,7 @@ parts:
- git - git
- sqlite3 - sqlite3
- xdg-user-dirs - xdg-user-dirs
- libqt6qml6
# libxcb-cursor0 workaround was needed against the Qt5/KF5 core22 content # libxcb-cursor0 workaround was needed against the Qt5/KF5 core22 content
# snap (issue #373). Not yet re-verified against kf6-core24 — remove this # snap (issue #373). Not yet re-verified against kf6-core24 — remove this
# if the Qt6 xcb platform plugin loads cleanly without it. # if the Qt6 xcb platform plugin loads cleanly without it.
@@ -84,6 +85,7 @@ parts:
- libsqlite3-dev - libsqlite3-dev
- qt6-tools-dev - qt6-tools-dev
- qt6-base-private-dev - qt6-base-private-dev
- qt6-declarative-dev
- pkgconf - pkgconf
# Qt6 PrintSupport records Cups::Cups as a third-party dependency # Qt6 PrintSupport records Cups::Cups as a third-party dependency
# (qprint_p.h includes <cups/ppd.h>), so find_package(Qt6 PrintSupport) # (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/qet.h
${QET_DIR}/sources/qeticons.cpp ${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/qeticons.h ${QET_DIR}/sources/qeticons.h
${QET_DIR}/sources/qetpalette.cpp
${QET_DIR}/sources/qetpalette.h
${QET_DIR}/sources/qetinformation.cpp ${QET_DIR}/sources/qetinformation.cpp
${QET_DIR}/sources/qetinformation.h ${QET_DIR}/sources/qetinformation.h
${QET_DIR}/sources/qetmainwindow.cpp ${QET_DIR}/sources/qetmainwindow.cpp
@@ -29,6 +29,9 @@ TerminalStripLayoutEditor::TerminalStripLayoutEditor(QSharedPointer<TerminalStri
{ {
ui->setupUi(this); ui->setupUi(this);
ui->m_graphics_view->setScene(new QGraphicsScene{ 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); ui->m_graphics_view->scene()->addItem(&m_preview_strip_item);
updateUi(); updateUi();
} }
+18 -14
View File
@@ -658,21 +658,14 @@ void ElementsPanelWidget::duplicateDiagram()
BorderProperties bp = source_diagram->border_and_titleblock.exportBorder(); BorderProperties bp = source_diagram->border_and_titleblock.exportBorder();
new_diagram->border_and_titleblock.importBorder(bp); new_diagram->border_and_titleblock.importBorder(bp);
for (QGraphicsItem *item : source_diagram->items()) { // Serialize the whole diagram with is_copy_command=true.
if (Element *elmt = dynamic_cast<Element *>(item)) { // This is the same mechanism as Ctrl+C: toXml(true, true)
source_diagram->correctTextPos(elmt); // internally calls correctTextPos/restoreText for Slave and
} // Report elements only — producing correct text positions
} // in the XML. No manual correctTextPos/restoreText needed.
QDomDocument doc = source_diagram->toXml(true, true);
QDomDocument doc = source_diagram->toXml();
QDomElement diagram_elmt = doc.documentElement(); QDomElement diagram_elmt = doc.documentElement();
for (QGraphicsItem *item : source_diagram->items()) {
if (Element *elmt = dynamic_cast<Element *>(item)) {
source_diagram->restoreText(elmt);
}
}
new_diagram->fromXml(diagram_elmt, QPointF(0, 0), false, nullptr); new_diagram->fromXml(diagram_elmt, QPointF(0, 0), false, nullptr);
for (QGraphicsItem *item : new_diagram->items()) { for (QGraphicsItem *item : new_diagram->items()) {
@@ -683,7 +676,18 @@ void ElementsPanelWidget::duplicateDiagram()
// of the project database, so duplicates fail to insert and // of the project database, so duplicates fail to insert and
// silently vanish from nomenclature/summary tables. // silently vanish from nomenclature/summary tables.
elmt->newUuid(); elmt->newUuid();
new_diagram->restoreText(elmt);
// toXml(true, true) applied correctTextPos to Slave and
// Report elements, which shifted their text positions to
// match the stripped composite text. restoreText()
// recalculates the position for the actual resolved text.
// Only Slave and Report need this — other element types
// were not affected by correctTextPos.
if (elmt->linkType() == Element::Slave ||
elmt->linkType() & Element::AllReport)
{
new_diagram->restoreText(elmt);
}
} }
else if (Conductor *cond = dynamic_cast<Conductor *>(item)) { else if (Conductor *cond = dynamic_cast<Conductor *>(item)) {
// Same reasoning for conductors: conductor.uuid is the PRIMARY // Same reasoning for conductors: conductor.uuid is the PRIMARY
+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 @brief ElementPictureFactory::pixmap
@param location @param location
+3 -2
View File
@@ -85,13 +85,14 @@ class ElementPictureFactory
void getPictures(const ElementsLocation &location, QPicture &picture, QPicture &low_picture); void getPictures(const ElementsLocation &location, QPicture &picture, QPicture &low_picture);
QPixmap pixmap(const ElementsLocation &location); QPixmap pixmap(const ElementsLocation &location);
ElementPictureFactory::primitives getPrimitives(const ElementsLocation &location); ElementPictureFactory::primitives getPrimitives(const ElementsLocation &location);
void dropCache(const ElementsLocation &location);
private: private:
ElementPictureFactory() {} ElementPictureFactory() {}
ElementPictureFactory (const ElementPictureFactory &); ElementPictureFactory (const ElementPictureFactory &);
ElementPictureFactory operator= (const ElementPictureFactory &); ElementPictureFactory operator= (const ElementPictureFactory &);
~ElementPictureFactory(); ~ElementPictureFactory();
static QUuid cacheKey(const ElementsLocation &location); static QUuid cacheKey(const ElementsLocation &location);
bool build(const ElementsLocation &location, QPicture *picture=nullptr, QPicture *low_picture=nullptr); bool build(const ElementsLocation &location, QPicture *picture=nullptr, QPicture *low_picture=nullptr);
void parseElement(const QDomElement &dom, QPainter &painter, primitives &prim) const; void parseElement(const QDomElement &dom, QPainter &painter, primitives &prim) const;
+40 -1
View File
@@ -26,6 +26,7 @@
#include "projectview.h" #include "projectview.h"
#include "qetdiagrameditor.h" #include "qetdiagrameditor.h"
#include "qeticons.h" #include "qeticons.h"
#include "qetpalette.h"
#include "utils/qetutils.h" #include "utils/qetutils.h"
#include "qetmessagebox.h" #include "qetmessagebox.h"
#include "qetproject.h" #include "qetproject.h"
@@ -50,6 +51,7 @@
#include <QFontDatabase> #include <QFontDatabase>
#include <QProcessEnvironment> #include <QProcessEnvironment>
#include <QRegularExpression> #include <QRegularExpression>
#include <QStyleHints>
#ifdef BUILD_WITHOUT_KF #ifdef BUILD_WITHOUT_KF
# include "ui/nokde/kautosavefile.h" # include "ui/nokde/kautosavefile.h"
#else #else
@@ -1786,8 +1788,10 @@ void QETApp::invertMainWindowVisibility(QWidget *window) {
false pour utiliser celles du theme en cours false pour utiliser celles du theme en cours
*/ */
void QETApp::useSystemPalette(bool use) { void QETApp::useSystemPalette(bool 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) { if (use) {
qApp->setPalette(initial_palette_);
// Drop any stylesheet previously loaded from style.css: with system // Drop any stylesheet previously loaded from style.css: with system
// colors requested, the palette set just above is what provides them. // colors requested, the palette set just above is what provides them.
// //
@@ -2341,9 +2345,44 @@ void QETApp::initStyle()
{ {
initial_palette_ = qApp->palette(); 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 //Apply or not the system style
QSettings settings; QSettings settings;
useSystemPalette(settings.value("usesystemcolors", true).toBool()); 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
@@ -25,6 +25,7 @@
#include "autoNum/ui/autonumberingdockwidget.h" #include "autoNum/ui/autonumberingdockwidget.h"
#include "conductornumexport.h" #include "conductornumexport.h"
#include "diagramcommands.h" #include "diagramcommands.h"
#include "diagramcontent.h"
#include "diagramevent/diagrameventaddimage.h" #include "diagramevent/diagrameventaddimage.h"
#ifdef QET_HAS_QTPDF #ifdef QET_HAS_QTPDF
#include "diagramevent/diagrameventaddpdf.h" #include "diagramevent/diagrameventaddpdf.h"
@@ -35,6 +36,7 @@
#include "diagramevent/diagrameventaddpaste.h" #include "diagramevent/diagrameventaddpaste.h"
#include "diagramview.h" #include "diagramview.h"
#include "elementspanelwidget.h" #include "elementspanelwidget.h"
#include "factory/elementpicturefactory.h"
#include "factory/qetgraphicstablefactory.h" #include "factory/qetgraphicstablefactory.h"
#include "print/projectprintwindow.h" #include "print/projectprintwindow.h"
#include "project/projectpropertieshandler.h" #include "project/projectpropertieshandler.h"
@@ -552,6 +554,13 @@ void QETDiagramEditor::setUpActions()
m_terminal_numbering = new QAction(QET::Icons::TerminalStrip, tr("Numérotation automatique des bornes"), this); 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); 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_HAS_SCRIPTING #ifdef QET_HAS_SCRIPTING
// Run a JavaScript macro against the current project (bugtracker #162). // Run a JavaScript macro against the current project (bugtracker #162).
m_run_script = new QAction(tr("Exécuter un script..."), this); m_run_script = new QAction(tr("Exécuter un script..."), this);
@@ -1014,6 +1023,7 @@ void QETDiagramEditor::setUpMenu()
menu_project -> addAction(m_project_export_wiring_list); menu_project -> addAction(m_project_export_wiring_list);
menu_project -> addAction(m_project_wiring_list_view); menu_project -> addAction(m_project_wiring_list_view);
menu_project -> addAction(m_terminal_numbering); menu_project -> addAction(m_terminal_numbering);
menu_project -> addAction(m_reload_element_drawings);
#ifdef QET_HAS_SCRIPTING #ifdef QET_HAS_SCRIPTING
menu_project -> addAction(m_run_script); menu_project -> addAction(m_run_script);
#endif #endif
@@ -1871,6 +1881,7 @@ void QETDiagramEditor::slot_updateActions()
m_project_export_wiring_list -> setEnabled(opened_project); m_project_export_wiring_list -> setEnabled(opened_project);
m_project_wiring_list_view -> setEnabled(opened_project); m_project_wiring_list_view -> setEnabled(opened_project);
m_terminal_numbering -> setEnabled(editable_project); m_terminal_numbering -> setEnabled(editable_project);
m_reload_element_drawings -> setEnabled(opened_project);
#ifdef QET_HAS_SCRIPTING #ifdef QET_HAS_SCRIPTING
m_run_script -> setEnabled(opened_project); m_run_script -> setEnabled(opened_project);
#endif #endif
@@ -2989,6 +3000,100 @@ 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();
}
#ifdef QET_HAS_SCRIPTING #ifdef QET_HAS_SCRIPTING
/** /**
@brief QETDiagramEditor::slot_runScript @brief QETDiagramEditor::slot_runScript
+2
View File
@@ -136,6 +136,7 @@ class QETDiagramEditor : public QETMainWindow
void editProjectProperties(ProjectView *); void editProjectProperties(ProjectView *);
void editProjectProperties(QETProject *); void editProjectProperties(QETProject *);
void slot_terminalNumbering(); void slot_terminalNumbering();
void slot_reloadElementDrawings();
#ifdef QET_HAS_SCRIPTING #ifdef QET_HAS_SCRIPTING
void slot_runScript(); void slot_runScript();
#endif #endif
@@ -214,6 +215,7 @@ class QETDiagramEditor : public QETMainWindow
*m_project_export_wiring_list, ///< Action to export the wiring list *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_project_wiring_list_view, ///< Action to show the wiring list read from the project database
*m_terminal_numbering, ///< Action to launch terminal numbering *m_terminal_numbering, ///< Action to launch terminal numbering
*m_reload_element_drawings, ///< Action to redraw every placed element from its current definition
#ifdef QET_HAS_SCRIPTING #ifdef QET_HAS_SCRIPTING
*m_run_script, ///< Action to run a JavaScript macro against the current project *m_run_script, ///< Action to run a JavaScript macro against the current project
#endif #endif
+133
View File
@@ -1784,6 +1784,139 @@ ElementsLocation Element::location() const
return m_location; 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 * @brief Element::updateConductorTexts
*Slot that is triggered when a cable is * *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::kind link_type = Element::Simple);
~Element() override; ~Element() override;
private: private:
bool definitionGeometryMatches(const QDomElement &definition) const;
Element(const Element &); Element(const Element &);
// attributes // attributes
@@ -153,6 +154,13 @@ class Element : public QetGraphicsItem
QString name() const override; QString name() const override;
ElementsLocation location() const; 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); virtual void setHighlighted(bool);
void displayHelpLine(bool b = true); void displayHelpLine(bool b = true);
QSize size() const; QSize size() const;
+6 -1
View File
@@ -86,7 +86,12 @@ Terminal::Terminal(TerminalData* data, Element* e) :
* Destruction of the terminal, and also docked conductor * Destruction of the terminal, and also docked conductor
*/ */
Terminal::~Terminal() { Terminal::~Terminal() {
qDeleteAll(m_conductors_list); // Each conductor's destructor calls removeConductor() on both its
// terminals, which mutates m_conductors_list while qDeleteAll() is
// still iterating it. Delete from a snapshot so the live list can
// change underneath without affecting the iteration.
const QList<Conductor *> conductors_to_delete = m_conductors_list;
qDeleteAll(conductors_to_delete);
delete d; delete d;
} }
+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")); QStandardItem *color = new QStandardItem(tr("Couleur"));
color->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); 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; QStandardItem *colora = new QStandardItem;
colora->setData(deti->color(), Qt::ForegroundRole); colora->setData(deti->color(), Qt::DecorationRole);
colora->setData(deti->color(), Qt::EditRole); colora->setData(deti->color(), Qt::EditRole);
colora->setData(DynamicElementTextModel::color, Qt::UserRole+1); colora->setData(DynamicElementTextModel::color, Qt::UserRole+1);
colora->setFlags(Qt::ItemIsSelectable colora->setFlags(Qt::ItemIsSelectable
@@ -1500,7 +1502,7 @@ void DynamicElementTextModel::updateDataFromText(DynamicElementTextItem *deti,
case color: case color:
{ {
qsi->child(color_txt_row,1)->setData(deti->color(), Qt::EditRole); 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; break;
} }
case pos: case pos:
@@ -1832,7 +1834,7 @@ void DynamicTextItemDelegate::setModelData(
{ {
QColor color = editor->property("color").value<QColor>(); QColor color = editor->property("color").value<QColor>();
qsi->setData(color, Qt::EditRole); qsi->setData(color, Qt::EditRole);
qsi->setData(color, Qt::ForegroundRole); qsi->setData(color, Qt::DecorationRole);
} }
return; 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_include_directories(tst_contactusage PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_contactusage PRIVATE Qt::Test) 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( add_executable(
tst_smart_device tst_smart_device
tst_smart_device.cpp 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"