#include "ui/ThemeIcons.h" #include #include #include #include #include #include #include #include #include namespace app { namespace { /* What a usable theme has to have; every window in the app needs it. */ const char kProbe[] = "mail-attachment"; /* The desktop's own theme first (whatever the platform theme set), then * Breeze: this window is dark, so the dark variant, and Breeze proper as * Qt's fallback for the odd name that only lives there. Before any of that * the search paths have to be filled in -- Qt only builds them from XDG once * a theme name was set, and a shell that was started outside the desktop * session (nix-shell) has no XDG_DATA_DIRS pointing at the icon packages, * hence the system profile. */ void adoptIconTheme() { const QStringList known = QStandardPaths::standardLocations( QStandardPaths::GenericDataLocation); QStringList paths; for (const QString &d : known) paths << d + QStringLiteral("/icons"); paths << QDir::homePath() + QStringLiteral("/.icons"); const QString system = QStringLiteral("/run/current-system/sw/share/icons"); if (!paths.contains(system) && QDir(system).exists()) paths << system; paths << QStringLiteral(":/icons"); QIcon::setThemeSearchPaths(paths); QStringList candidates; if (!QIcon::themeName().isEmpty()) candidates << QIcon::themeName(); candidates << QStringLiteral("breeze-dark") << QStringLiteral("breeze") << QStringLiteral("hicolor"); for (const QString &name : candidates) { QIcon::setThemeName(name); if (QIcon::hasThemeIcon(QLatin1String(kProbe))) return; } } /* The bundled set: app/icons/*.svg, compiled in as :/cst-icons/.svg by * app/CMakeLists.txt. A machine without an icon theme -- Android, where the * Qt theme lookup finds nothing and QML was left showing its glyph fallback, * which the phone fonts cannot draw -- gets the same icons from here. */ QIcon bundledIcon(const QString &name) { return QIcon(QStringLiteral(":/cst-icons/") + name + QStringLiteral(".svg")); } QIcon iconForName(const QString &name) { const QIcon themed = QIcon::fromTheme(name); if (!themed.isNull()) return themed; return bundledIcon(name); } /* "?color=%23eff0f1": the tint, and nothing else so far. */ QColor colorOption(const QString &id, QString *name) { QColor color; *name = id; const int q = id.indexOf(QLatin1Char('?')); if (q < 0) return color; *name = id.left(q); const QStringList options = id.mid(q + 1).split(QLatin1Char('&')); for (const QString &opt : options) { if (!opt.startsWith(QLatin1String("color="))) continue; const QColor c(QUrl::fromPercentEncoding(opt.mid(6).toUtf8())); if (c.isValid()) color = c; } return color; } qreal targetRatio() { if (const QScreen *s = QGuiApplication::primaryScreen()) return qMax(1.0, s->devicePixelRatio()); return 1.0; } } // namespace ThemeIconProvider::ThemeIconProvider() : QQuickImageProvider(QQuickImageProvider::Image) {} QImage ThemeIconProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) { QString name; const QColor color = colorOption(id, &name); if (name.isEmpty()) return QImage(); const QIcon icon = iconForName(name); if (icon.isNull()) return QImage(); /* QML shows its fallback glyph */ const int px = requestedSize.isValid() && requestedSize.width() > 0 ? requestedSize.width() : 22; const qreal ratio = targetRatio(); QPixmap pm = icon.pixmap(QSize(px, px), ratio, QIcon::Normal, QIcon::Off); if (pm.isNull()) return QImage(); if (color.isValid()) { QPainter p(&pm); p.setCompositionMode(QPainter::CompositionMode_SourceIn); p.fillRect(pm.rect(), color); } QImage img = pm.toImage(); img.setDevicePixelRatio(ratio); if (size) *size = QSize(px, px); return img; } void installThemeIcons(QQmlApplicationEngine *engine) { adoptIconTheme(); if (engine) engine->addImageProvider(QStringLiteral("csttheme"), new ThemeIconProvider); } QString iconThemeName() { return QIcon::themeName(); } bool themeIconAvailable(const QString &name) { return !iconForName(name).isNull(); } } // namespace app