mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-29 05:44:14 +02:00
d06c7be606
misc/make_icon_themes.py sorted icons by saturation alone, so a white page with a small red mark counted as line art and its dark copy turned the page black: the PDF import icon read black on black (#919, Kellermorph), and the folio, diagram and label icons came out as dark pages with a light border. An icon whose visible pixels are at least 30% near white is now "light art" and inherits from the qet theme untouched; it already reads on a dark toolbar. The generator also removes dark files it no longer produces, so a reclassified icon falls back to the light theme instead of keeping a stale copy. Thirteen files leave ico/themes/qet-dark. tests/qttest/tst_qeticons: every dark theme file, taken as its mean visible color, reaches 3:1 on the dark palette's window color; the lightest-pixel check it replaces let a black page with a light border through. Asking the dark theme for pdf-import, diagram, label, the folio icons and diagram_bg returns the light art.
263 lines
9.3 KiB
C++
263 lines
9.3 KiB
C++
/*
|
|
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 <QDir>
|
|
#include <QFile>
|
|
#include <QIcon>
|
|
#include <QMainWindow>
|
|
#include <QStyleFactory>
|
|
#include <QToolBar>
|
|
#include <QToolButton>
|
|
|
|
#include "inkcontrast.h"
|
|
#include "qeticons.h"
|
|
#include "qetpalette.h"
|
|
|
|
/**
|
|
Checks on QET's two icon themes, "qet" and "qet-dark", generated by
|
|
misc/make_icon_themes.py into ico/icon-themes.qrc, and on the icon
|
|
table that uses them.
|
|
|
|
Every icon name must resolve in both themes. The dark theme's files
|
|
must read on the dark palette, and an icon drawn as a light object
|
|
(a page sheet, the PDF import icon of GitHub #919) must come through
|
|
the dark theme untouched rather than inverted into a black page. A
|
|
toolbar button painted with Fusion must show its icon at 3:1 (WCAG
|
|
1.4.11) in both themes, with the disabled state reading weaker than
|
|
the enabled one, which is what was reversed on macOS before (GitHub
|
|
#466, #870). And the icons the elements panel draws in its 50 px
|
|
slots must stay small.
|
|
*/
|
|
class tst_qeticons : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
private slots:
|
|
void initTestCase();
|
|
void everyIconResolvesInBothThemes();
|
|
void darkThemeFilesReadOnDarkPalette();
|
|
void lightIconsStayLightInDarkTheme();
|
|
void toolbarIconIsReadable_data();
|
|
void toolbarIconIsReadable();
|
|
void panelProjectIconStaysSmall();
|
|
};
|
|
|
|
namespace {
|
|
const double kIconRatio = 3.0;
|
|
|
|
QStringList iconNames(const QString &theme)
|
|
{
|
|
QStringList names;
|
|
for (const QString &size : {"16x16", "22x22", "32x32", "48x48", "128x128", "scalable"})
|
|
{
|
|
QDir dir(QString(":/ico/themes/%1/%2").arg(theme, size));
|
|
for (const QString &file : dir.entryList(QDir::Files))
|
|
names << file.section('.', 0, -2);
|
|
}
|
|
names.removeDuplicates();
|
|
names.sort();
|
|
return names;
|
|
}
|
|
|
|
/**
|
|
Mean color of the visible pixels: what the icon reads as, taken
|
|
as a whole, on the background behind it. A page-shaped icon whose
|
|
white body was inverted keeps a light border, so the lightest
|
|
pixel would pass it; the mean does not.
|
|
*/
|
|
QColor meanVisibleColor(const QImage &image)
|
|
{
|
|
qint64 r = 0, g = 0, b = 0, n = 0;
|
|
for (int y = 0; y < image.height(); ++y)
|
|
for (int x = 0; x < image.width(); ++x)
|
|
{
|
|
const QColor c = image.pixelColor(x, y);
|
|
if (c.alpha() <= 64) continue;
|
|
r += c.red(); g += c.green(); b += c.blue(); ++n;
|
|
}
|
|
return n ? QColor(r / n, g / n, b / n) : QColor();
|
|
}
|
|
|
|
const QStringList kSizes = {"16x16", "22x22", "32x32", "48x48", "128x128"};
|
|
}
|
|
|
|
void tst_qeticons::initTestCase()
|
|
{
|
|
QStringList paths = QIcon::themeSearchPaths();
|
|
paths.prepend(QStringLiteral(":/ico/themes"));
|
|
QIcon::setThemeSearchPaths(paths);
|
|
QIcon::setThemeName(QStringLiteral("qet"));
|
|
QVERIFY2(!iconNames("qet").isEmpty(), "light theme has no icons: is ico/icon-themes.qrc compiled in?");
|
|
}
|
|
|
|
void tst_qeticons::everyIconResolvesInBothThemes()
|
|
{
|
|
const QStringList names = iconNames("qet");
|
|
for (const QString &theme : {"qet", "qet-dark"})
|
|
{
|
|
QIcon::setThemeName(theme);
|
|
QVERIFY(QIcon::hasThemeIcon("list-add"));
|
|
for (const QString &name : names)
|
|
QVERIFY2(!QIcon::fromTheme(name).isNull(),
|
|
qPrintable(QString("%1 missing in theme %2").arg(name, theme)));
|
|
}
|
|
// Names the icon table relies on that come from the alias list in
|
|
// misc/make_icon_themes.py, not from a file of that name at 22 pixels.
|
|
QIcon::setThemeName("qet");
|
|
for (const QString &name : {"folio-new", "folio-delete", "folio-properties", "conductor-reset"})
|
|
QVERIFY2(!QIcon::fromTheme(name).pixmap(22).isNull(), qPrintable(name));
|
|
}
|
|
|
|
/**
|
|
Every file the generator put in the dark theme, taken as a whole,
|
|
must reach 3:1 against the dark palette's window color. Inverted
|
|
line art passes by a wide margin; an inverted light object (a white
|
|
page turned black) sits below 2:1 and fails, which is the defect
|
|
misc/make_icon_themes.py now avoids by leaving such icons to the
|
|
light theme.
|
|
*/
|
|
void tst_qeticons::darkThemeFilesReadOnDarkPalette()
|
|
{
|
|
const QColor window = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Window);
|
|
int checked = 0;
|
|
for (const QString &size : kSizes)
|
|
{
|
|
QDir dir(QString(":/ico/themes/qet-dark/%1").arg(size));
|
|
for (const QString &file : dir.entryList({"*.png"}, QDir::Files))
|
|
{
|
|
const QImage dark(dir.filePath(file));
|
|
QVERIFY2(!dark.isNull(), qPrintable(file));
|
|
const QColor mean = meanVisibleColor(dark);
|
|
QVERIFY2(mean.isValid(), qPrintable(file));
|
|
const double contrast = QET::Palette::contrastRatio(mean, window);
|
|
QVERIFY2(contrast >= kIconRatio,
|
|
qPrintable(QString("%1/%2 reads %3:1 on the dark window").arg(size, file).arg(contrast)));
|
|
++checked;
|
|
}
|
|
}
|
|
QVERIFY(checked > 100);
|
|
}
|
|
|
|
/**
|
|
Icons drawn as a light object are not in the dark theme; the theme
|
|
inherits them from "qet". Asking the dark theme for them must return
|
|
the light art, not a dark page (GitHub #919, the PDF import icon).
|
|
*/
|
|
void tst_qeticons::lightIconsStayLightInDarkTheme()
|
|
{
|
|
const QColor window = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Window);
|
|
QIcon::setThemeName(QStringLiteral("qet-dark"));
|
|
for (const QString &name : {"pdf-import", "diagram", "label", "folio-new", "folio-delete",
|
|
"folio-properties", "diagram_bg"})
|
|
{
|
|
QVERIFY2(!QFile::exists(QString(":/ico/themes/qet-dark/22x22/%1.png").arg(name)),
|
|
qPrintable(QString("%1 has a dark copy; the generator inverted a light icon").arg(name)));
|
|
const QImage image = QIcon::fromTheme(name).pixmap(22).toImage();
|
|
QVERIFY2(!image.isNull(), qPrintable(name));
|
|
const QColor mean = meanVisibleColor(image);
|
|
QVERIFY2(mean.lightnessF() > 0.6,
|
|
qPrintable(QString("%1 comes out dark in the dark theme (lightness %2)")
|
|
.arg(name).arg(mean.lightnessF())));
|
|
QVERIFY2(QET::Palette::contrastRatio(mean, window) >= kIconRatio, qPrintable(name));
|
|
}
|
|
}
|
|
|
|
void tst_qeticons::toolbarIconIsReadable_data()
|
|
{
|
|
QTest::addColumn<QString>("theme");
|
|
QTest::addColumn<QPalette>("palette");
|
|
QTest::newRow("light") << "qet" << QET::Palette::fusionLight();
|
|
QTest::newRow("dark") << "qet-dark" << QET::Palette::fusionDark();
|
|
}
|
|
|
|
/**
|
|
A line-art icon on a Fusion tool button, as in the diagram editor
|
|
toolbar. Enabled must reach 3:1; disabled must be weaker than enabled.
|
|
*/
|
|
void tst_qeticons::toolbarIconIsReadable()
|
|
{
|
|
QFETCH(QString, theme);
|
|
QFETCH(QPalette, palette);
|
|
QIcon::setThemeName(theme);
|
|
QApplication::setStyle(QStyleFactory::create("Fusion"));
|
|
QApplication::setPalette(palette);
|
|
|
|
QMainWindow window;
|
|
QToolBar *toolbar = window.addToolBar("view");
|
|
auto make = [&](const char *name, bool enabled) {
|
|
auto *button = new QToolButton;
|
|
button->setIcon(QIcon::fromTheme("zoom-fit-best"));
|
|
button->setAutoRaise(true);
|
|
button->setEnabled(enabled);
|
|
button->setObjectName(name);
|
|
toolbar->addWidget(button);
|
|
return button;
|
|
};
|
|
QToolButton *enabled = make("enabled", true);
|
|
QToolButton *disabled = make("disabled", false);
|
|
window.resize(300, 100);
|
|
window.show();
|
|
QVERIFY(QTest::qWaitForWindowExposed(&window));
|
|
|
|
const double enabled_contrast = QET::Test::inkContrast(QET::Test::grab(enabled, "enabled"), enabled->rect());
|
|
const double disabled_contrast = QET::Test::inkContrast(QET::Test::grab(disabled, "disabled"), disabled->rect());
|
|
QVERIFY2(enabled_contrast >= kIconRatio,
|
|
qPrintable(QString("enabled icon: %1").arg(enabled_contrast)));
|
|
QVERIFY2(disabled_contrast < enabled_contrast,
|
|
qPrintable(QString("disabled %1 reads better than enabled %2")
|
|
.arg(disabled_contrast).arg(enabled_contrast)));
|
|
}
|
|
|
|
/**
|
|
The elements panel sets a 50 px icon size so element previews are
|
|
large. Its project root item uses ProjectFileGP, whose theme name also
|
|
has a 128 px file for the configuration dialog. On a 2x display the
|
|
theme loader would pick that file for a 50 px request and fill the
|
|
slot, so the entry must not resolve through the theme name.
|
|
*/
|
|
void tst_qeticons::panelProjectIconStaysSmall()
|
|
{
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
QSKIP("QIcon::pixmap(size, devicePixelRatio) needs Qt 6");
|
|
#else
|
|
QIcon::setThemeName(QStringLiteral("qet"));
|
|
QET::Icons::initIcons();
|
|
|
|
const QPixmap panel = QET::Icons::ProjectFileGP.pixmap(QSize(50, 50), 2.0);
|
|
QVERIFY(!panel.isNull());
|
|
const int logical = qRound(panel.width() / panel.devicePixelRatio());
|
|
QVERIFY2(logical <= 22, qPrintable(QString("project root icon is %1 px in a 50 px slot").arg(logical)));
|
|
|
|
// The configuration dialog's page list still gets the large file.
|
|
QCOMPARE(QET::Icons::Projects.pixmap(QSize(128, 128), 1.0).width(), 128);
|
|
#endif
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM"))
|
|
qputenv("QT_QPA_PLATFORM", "offscreen");
|
|
QApplication app(argc, argv);
|
|
tst_qeticons test;
|
|
QTEST_SET_MAIN_SOURCE_PATH
|
|
return QTest::qExec(&test, argc, argv);
|
|
}
|
|
|
|
#include "tst_qeticons.moc"
|