mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-20 15:24:14 +02:00
Compare commits
9 Commits
451f8c3296
...
e7e2864641
| Author | SHA1 | Date | |
|---|---|---|---|
| e7e2864641 | |||
| 6562498ca2 | |||
| 38e504fb7e | |||
| 4aef4d17d2 | |||
| c89243a36f | |||
| b208a4f131 | |||
| 1c8e7ed052 | |||
| 9004db1d99 | |||
| 43d27a9563 |
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+1
-1
Submodule elements updated: 99c4d51b2c...60eacc28a6
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,6 +22,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"
|
||||||
@@ -32,6 +33,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"
|
||||||
@@ -549,6 +551,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_EXPORT_PROJECT_DB
|
#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);
|
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]() {
|
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_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_EXPORT_PROJECT_DB
|
#ifdef QET_EXPORT_PROJECT_DB
|
||||||
menu_project -> addSeparator();
|
menu_project -> addSeparator();
|
||||||
menu_project -> addAction(m_export_project_db);
|
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_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_EXPORT_PROJECT_DB
|
#ifdef QET_EXPORT_PROJECT_DB
|
||||||
m_export_project_db -> setEnabled(editable_project);
|
m_export_project_db -> setEnabled(editable_project);
|
||||||
#endif
|
#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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
void editDiagramProperties(DiagramView *);
|
void editDiagramProperties(DiagramView *);
|
||||||
void editDiagramProperties(Diagram *);
|
void editDiagramProperties(Diagram *);
|
||||||
void addDiagramToProject(QETProject *);
|
void addDiagramToProject(QETProject *);
|
||||||
@@ -211,6 +212,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
|
||||||
*m_export_project_db, ///Export to file the internal database of the current project
|
*m_export_project_db, ///Export to file the internal database of the current project
|
||||||
*m_tile_window, ///< Show MDI subwindows as tile
|
*m_tile_window, ///< Show MDI subwindows as tile
|
||||||
*m_cascade_window, ///< Show MDI subwindows as cascade
|
*m_cascade_window, ///< Show MDI subwindows as cascade
|
||||||
|
|||||||
@@ -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 *
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user