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
@@ -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();
}
+18 -14
View File
@@ -658,21 +658,14 @@ void ElementsPanelWidget::duplicateDiagram()
BorderProperties bp = source_diagram->border_and_titleblock.exportBorder();
new_diagram->border_and_titleblock.importBorder(bp);
for (QGraphicsItem *item : source_diagram->items()) {
if (Element *elmt = dynamic_cast<Element *>(item)) {
source_diagram->correctTextPos(elmt);
}
}
QDomDocument doc = source_diagram->toXml();
// Serialize the whole diagram with is_copy_command=true.
// This is the same mechanism as Ctrl+C: toXml(true, true)
// 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);
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);
for (QGraphicsItem *item : new_diagram->items()) {
@@ -683,7 +676,18 @@ void ElementsPanelWidget::duplicateDiagram()
// of the project database, so duplicates fail to insert and
// silently vanish from nomenclature/summary tables.
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)) {
// 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
@param location
+3 -2
View File
@@ -85,13 +85,14 @@ 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() {}
ElementPictureFactory (const ElementPictureFactory &);
ElementPictureFactory operator= (const ElementPictureFactory &);
~ElementPictureFactory();
static QUuid cacheKey(const ElementsLocation &location);
bool build(const ElementsLocation &location, QPicture *picture=nullptr, QPicture *low_picture=nullptr);
void parseElement(const QDomElement &dom, QPainter &painter, primitives &prim) const;
+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) {
// 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) {
qApp->setPalette(initial_palette_);
// 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
@@ -25,6 +25,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"
@@ -35,6 +36,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"
@@ -552,6 +554,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_HAS_SCRIPTING
// Run a JavaScript macro against the current project (bugtracker #162).
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_wiring_list_view);
menu_project -> addAction(m_terminal_numbering);
menu_project -> addAction(m_reload_element_drawings);
#ifdef QET_HAS_SCRIPTING
menu_project -> addAction(m_run_script);
#endif
@@ -1871,6 +1881,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_HAS_SCRIPTING
m_run_script -> setEnabled(opened_project);
#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
/**
@brief QETDiagramEditor::slot_runScript
+2
View File
@@ -136,6 +136,7 @@ class QETDiagramEditor : public QETMainWindow
void editProjectProperties(ProjectView *);
void editProjectProperties(QETProject *);
void slot_terminalNumbering();
void slot_reloadElementDrawings();
#ifdef QET_HAS_SCRIPTING
void slot_runScript();
#endif
@@ -214,6 +215,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
#ifdef QET_HAS_SCRIPTING
*m_run_script, ///< Action to run a JavaScript macro against the current project
#endif
+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;
+6 -1
View File
@@ -86,7 +86,12 @@ Terminal::Terminal(TerminalData* data, Element* e) :
* Destruction of the terminal, and also docked conductor
*/
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;
}
+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;
}