mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-03 01:04:13 +02:00
800ddfbcbe
189 of QET's 266 fixed-size icons are black line art with no dark variant, so on a dark palette they were black on a dark toolbar, and Fusion's disabled rendering lightened them into something more readable than the enabled state (GitHub #466, #870; bugtracker 335 for the element panels, which keep their own fix). The theme "qet-dark" holds light-ink copies of the line-art icons in ico/themes/qet-dark, generated by misc/make_icon_themes.py. Colored icons are not copied; the theme inherits them from "qet". An icon counts as line art when fewer than 20% of its visible pixels are saturated. The copies keep hue and alpha and invert lightness, scaled so each icon's darkest ink becomes (220,220,220), the dark palette's text color. The eight SVG icons get their color replaced the same way. QETApp::applyIconTheme() picks "qet-dark" for a dark palette and "qet" otherwise. It runs from initIconTheme(), again from initStyle() once the palette is final, and on the OS color scheme switch. Icons created with QIcon::fromTheme() re-resolve on their next paint, so nothing else changes. With light-ink files, Fusion's own disabled rendering comes out dimmer than enabled with no extra code. tests/qttest/tst_qeticons: every name resolves in both themes, every dark file has light ink, and a Fusion tool button shows its icon at 3:1 in both themes with disabled weaker than enabled.
217 lines
7.2 KiB
C++
217 lines
7.2 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 <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 be light ink. 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 darkThemeFilesAreLightInk();
|
|
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;
|
|
}
|
|
|
|
/// Lightness of the lightest visible pixel: the ink, in a dark-theme file.
|
|
double maxLightness(const QImage &image)
|
|
{
|
|
double max = 0.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) max = qMax(max, c.lightnessF());
|
|
}
|
|
return max;
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
void tst_qeticons::darkThemeFilesAreLightInk()
|
|
{
|
|
int checked = 0;
|
|
for (const QString &size : {"16x16", "22x22", "32x32"})
|
|
{
|
|
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));
|
|
const QImage light(QString(":/ico/themes/qet/%1/%2").arg(size, file));
|
|
QVERIFY2(!dark.isNull() && !light.isNull(), qPrintable(file));
|
|
// A page-shaped icon keeps its white fill dark after inversion,
|
|
// so the mean would mislead; what matters is that the ink is light.
|
|
QVERIFY2(maxLightness(dark) >= 0.8,
|
|
qPrintable(QString("%1/%2 has no light ink").arg(size, file)));
|
|
++checked;
|
|
}
|
|
}
|
|
QVERIFY(checked > 100);
|
|
}
|
|
|
|
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"
|