mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-01 23:54:13 +02:00
ba29dc45a0
The "Add PDF" action used ico/22x22/pdf-import.png, a white page with a red PDF mark that sat apart from its neighbors "Add text" and "Add image", both gray line art in a square frame with a plus. It is now ico/scalable/pdf-import.svg, the same pixel design as insert-image.png: a frame, the letters PDF, a plus in the corner. One file serves every slot and stays sharp on high-DPI screens; the PNG is gone from both .qrc files. The canvas is 24 pixels with the art offset by one, like the Breeze SVGs already in the theme. Fusion's toolbar slot is 24 pixels: a 22 pixel PNG is drawn unscaled inside it, but a scalable icon is rendered at the slot size, and a 22 pixel grid stretched to 24 puts every one pixel line between pixels and reads blurry. The file uses currentColor like the Breeze SVGs already in the theme, so misc/make_icon_themes.py produces the dark copy the same way. A new ico/scalable/ folder holds QET's own vector icons; the direction arummler asked for in #690. tests/qttest/tst_qeticons: the icon resolves in both themes at 16, 22, 32 and 64 pixels, dark ink on the light theme and light ink on the dark one, and no 22 pixel PNG remains.
300 lines
11 KiB
C++
300 lines
11 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 addPdfIconIsOneScalableFile();
|
|
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 : {"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));
|
|
}
|
|
}
|
|
|
|
/**
|
|
The "Add PDF" icon is the first of QET's own icons drawn as an SVG:
|
|
ico/scalable/pdf-import.svg, one file for every size, with a
|
|
recolored copy in the dark theme. It must resolve in both themes at
|
|
the toolbar size and above, dark ink on light and light ink on dark.
|
|
Fusion's toolbar slot is 24 px, so the file is drawn on a 24 px
|
|
canvas and its one pixel lines land on whole pixels there.
|
|
*/
|
|
void tst_qeticons::addPdfIconIsOneScalableFile()
|
|
{
|
|
QVERIFY(QFile::exists(":/ico/themes/qet/scalable/pdf-import.svg"));
|
|
QVERIFY(QFile::exists(":/ico/themes/qet-dark/scalable/pdf-import.svg"));
|
|
QVERIFY(!QFile::exists(":/ico/themes/qet/22x22/pdf-import.png"));
|
|
|
|
const QByteArray dump = qgetenv("QET_TEST_DUMP_DIR");
|
|
for (const QString &theme : {"qet", "qet-dark"})
|
|
{
|
|
QIcon::setThemeName(theme);
|
|
const QIcon icon = QIcon::fromTheme("pdf-import");
|
|
QVERIFY2(!icon.isNull(), qPrintable(theme));
|
|
for (int size : {16, 22, 24, 32, 64})
|
|
{
|
|
const QPixmap pixmap = icon.pixmap(size);
|
|
QCOMPARE(pixmap.width(), size);
|
|
if (!dump.isEmpty())
|
|
pixmap.save(QString("%1/pdf-import-%2-%3.png").arg(QString::fromLocal8Bit(dump), theme).arg(size));
|
|
const QColor mean = meanVisibleColor(pixmap.toImage());
|
|
QVERIFY2(mean.isValid(), qPrintable(QString("%1 at %2 px is empty").arg(theme).arg(size)));
|
|
if (theme == "qet")
|
|
QVERIFY2(mean.lightnessF() < 0.5, qPrintable(QString("light theme at %1 px: lightness %2").arg(size).arg(mean.lightnessF())));
|
|
else
|
|
QVERIFY2(mean.lightnessF() > 0.6, qPrintable(QString("dark theme at %1 px: lightness %2").arg(size).arg(mean.lightnessF())));
|
|
}
|
|
}
|
|
}
|
|
|
|
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"
|