File indexing completed on 2024-04-28 15:27:42

0001 /*
0002  *  SPDX-FileCopyrightText: 2011 Marco Martin <mart@kde.org>
0003  *  SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
0004  *
0005  *  SPDX-License-Identifier: LGPL-2.0-or-later
0006  */
0007 
0008 #include "icon.h"
0009 #include "libkirigami/platformtheme.h"
0010 #include "scenegraph/managedtexturenode.h"
0011 
0012 #include "loggingcategory.h"
0013 #include <QBitmap>
0014 #include <QDebug>
0015 #include <QGuiApplication>
0016 #include <QIcon>
0017 #include <QNetworkReply>
0018 #include <QPainter>
0019 #include <QQuickImageProvider>
0020 #include <QQuickWindow>
0021 #include <QSGSimpleTextureNode>
0022 #include <QSGTexture>
0023 #include <QScreen>
0024 
0025 #include <cmath>
0026 
0027 Q_GLOBAL_STATIC(ImageTexturesCache, s_iconImageCache)
0028 
0029 Icon::Icon(QQuickItem *parent)
0030     : QQuickItem(parent)
0031     , m_changed(false)
0032     , m_active(false)
0033     , m_selected(false)
0034     , m_isMask(false)
0035 {
0036     setFlag(ItemHasContents, true);
0037     // Using 32 because Icon used to redefine implicitWidth and implicitHeight and hardcode them to 32
0038     setImplicitSize(32, 32);
0039     // FIXME: not necessary anymore
0040     connect(qApp, &QGuiApplication::paletteChanged, this, &QQuickItem::polish);
0041     connect(this, &QQuickItem::enabledChanged, this, &QQuickItem::polish);
0042     connect(this, &QQuickItem::smoothChanged, this, &QQuickItem::polish);
0043 }
0044 
0045 Icon::~Icon()
0046 {
0047 }
0048 
0049 void Icon::setSource(const QVariant &icon)
0050 {
0051     if (m_source == icon) {
0052         return;
0053     }
0054     m_source = icon;
0055     m_monochromeHeuristics.clear();
0056 
0057     if (!m_theme) {
0058         m_theme = static_cast<Kirigami::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(this, true));
0059         Q_ASSERT(m_theme);
0060 
0061         connect(m_theme, &Kirigami::PlatformTheme::colorsChanged, this, &QQuickItem::polish);
0062     }
0063 
0064     if (icon.type() == QVariant::String) {
0065         const QString iconSource = icon.toString();
0066         updateIsMaskHeuristic(iconSource);
0067         Q_EMIT isMaskChanged();
0068     }
0069 
0070     if (m_networkReply) {
0071         // if there was a network query going on, interrupt it
0072         m_networkReply->close();
0073     }
0074     m_loadedImage = QImage();
0075     setStatus(Loading);
0076 
0077     polish();
0078     Q_EMIT sourceChanged();
0079     Q_EMIT validChanged();
0080 }
0081 
0082 QVariant Icon::source() const
0083 {
0084     return m_source;
0085 }
0086 
0087 void Icon::setActive(const bool active)
0088 {
0089     if (active == m_active) {
0090         return;
0091     }
0092     m_active = active;
0093     polish();
0094     Q_EMIT activeChanged();
0095 }
0096 
0097 bool Icon::active() const
0098 {
0099     return m_active;
0100 }
0101 
0102 bool Icon::valid() const
0103 {
0104     // TODO: should this be return m_status == Ready?
0105     // Consider an empty URL invalid, even though isNull() will say false
0106     if (m_source.canConvert<QUrl>() && m_source.toUrl().isEmpty()) {
0107         return false;
0108     }
0109 
0110     return !m_source.isNull();
0111 }
0112 
0113 void Icon::setSelected(const bool selected)
0114 {
0115     if (selected == m_selected) {
0116         return;
0117     }
0118     m_selected = selected;
0119     polish();
0120     Q_EMIT selectedChanged();
0121 }
0122 
0123 bool Icon::selected() const
0124 {
0125     return m_selected;
0126 }
0127 
0128 void Icon::setIsMask(bool mask)
0129 {
0130     if (m_isMask == mask) {
0131         return;
0132     }
0133 
0134     m_isMask = mask;
0135     m_isMaskHeuristic = mask;
0136     polish();
0137     Q_EMIT isMaskChanged();
0138 }
0139 
0140 bool Icon::isMask() const
0141 {
0142     return m_isMask || m_isMaskHeuristic;
0143 }
0144 
0145 void Icon::setColor(const QColor &color)
0146 {
0147     if (m_color == color) {
0148         return;
0149     }
0150 
0151     m_color = color;
0152     polish();
0153     Q_EMIT colorChanged();
0154 }
0155 
0156 QColor Icon::color() const
0157 {
0158     return m_color;
0159 }
0160 
0161 QSGNode *Icon::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData * /*data*/)
0162 {
0163     if (m_source.isNull() || qFuzzyIsNull(width()) || qFuzzyIsNull(height())) {
0164         delete node;
0165         return Q_NULLPTR;
0166     }
0167 
0168     if (m_changed || node == nullptr) {
0169         const QSize itemSize(width(), height());
0170         QRect nodeRect(QPoint(0, 0), itemSize);
0171 
0172         ManagedTextureNode *mNode = dynamic_cast<ManagedTextureNode *>(node);
0173         if (!mNode) {
0174             delete node;
0175             mNode = new ManagedTextureNode;
0176         }
0177         if (itemSize.width() != 0 && itemSize.height() != 0) {
0178             mNode->setTexture(s_iconImageCache->loadTexture(window(), m_icon, QQuickWindow::TextureCanUseAtlas));
0179             if (m_icon.size() != itemSize) {
0180                 // At this point, the image will already be scaled, but we need to output it in
0181                 // the correct aspect ratio, painted centered in the viewport. So:
0182                 QRect destination(QPoint(0, 0), m_icon.size().scaled(itemSize, Qt::KeepAspectRatio));
0183                 destination.moveCenter(nodeRect.center());
0184                 nodeRect = destination;
0185             }
0186         }
0187         mNode->setRect(nodeRect);
0188         node = mNode;
0189         if (smooth()) {
0190             mNode->setFiltering(QSGTexture::Linear);
0191         }
0192         m_changed = false;
0193     }
0194 
0195     return node;
0196 }
0197 
0198 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0199 void Icon::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
0200 {
0201     QQuickItem::geometryChanged(newGeometry, oldGeometry);
0202 #else
0203 void Icon::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
0204 {
0205     QQuickItem::geometryChange(newGeometry, oldGeometry);
0206 #endif
0207     if (newGeometry.size() != oldGeometry.size()) {
0208         polish();
0209     }
0210 }
0211 
0212 void Icon::handleRedirect(QNetworkReply *reply)
0213 {
0214     QNetworkAccessManager *qnam = reply->manager();
0215     if (reply->error() != QNetworkReply::NoError) {
0216         return;
0217     }
0218     const QUrl possibleRedirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
0219     if (!possibleRedirectUrl.isEmpty()) {
0220         const QUrl redirectUrl = reply->url().resolved(possibleRedirectUrl);
0221         if (redirectUrl == reply->url()) {
0222             // no infinite redirections thank you very much
0223             reply->deleteLater();
0224             return;
0225         }
0226         reply->deleteLater();
0227         QNetworkRequest request(possibleRedirectUrl);
0228         request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
0229         m_networkReply = qnam->get(request);
0230         connect(m_networkReply.data(), &QNetworkReply::finished, this, [this]() {
0231             handleFinished(m_networkReply);
0232         });
0233     }
0234 }
0235 
0236 void Icon::handleFinished(QNetworkReply *reply)
0237 {
0238     if (!reply) {
0239         return;
0240     }
0241 
0242     reply->deleteLater();
0243     if (!reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isNull()) {
0244         handleRedirect(reply);
0245         return;
0246     }
0247 
0248     m_loadedImage = QImage();
0249 
0250     const QString filename = reply->url().fileName();
0251     if (!m_loadedImage.load(reply, filename.mid(filename.indexOf(QLatin1Char('.'))).toLatin1().constData())) {
0252         qCWarning(KirigamiLog) << "received broken image" << reply->url();
0253 
0254         // broken image from data, inform the user of this with some useful broken-image thing...
0255         const QIcon icon = QIcon::fromTheme(m_fallback);
0256         m_loadedImage = icon.pixmap(window(), icon.actualSize(size().toSize()), iconMode(), QIcon::On).toImage();
0257     }
0258 
0259     polish();
0260 }
0261 
0262 void Icon::updatePolish()
0263 {
0264     QQuickItem::updatePolish();
0265 
0266     if (m_source.isNull()) {
0267         setStatus(Ready);
0268         updatePaintedGeometry();
0269         update();
0270         return;
0271     }
0272 
0273     const QSize itemSize(width(), height());
0274     if (itemSize.width() != 0 && itemSize.height() != 0) {
0275         const auto multiplier = QCoreApplication::instance()->testAttribute(Qt::AA_UseHighDpiPixmaps)
0276             ? 1
0277             : (window() ? window()->effectiveDevicePixelRatio() : qGuiApp->devicePixelRatio());
0278         const QSize size = itemSize * multiplier;
0279 
0280         switch (m_source.type()) {
0281         case QVariant::Pixmap:
0282             m_icon = m_source.value<QPixmap>().toImage();
0283             break;
0284         case QVariant::Image:
0285             m_icon = m_source.value<QImage>();
0286             break;
0287         case QVariant::Bitmap:
0288             m_icon = m_source.value<QBitmap>().toImage();
0289             break;
0290         case QVariant::Icon: {
0291             const QIcon icon = m_source.value<QIcon>();
0292             m_icon = icon.pixmap(window(), icon.actualSize(itemSize), iconMode(), QIcon::On).toImage();
0293             break;
0294         }
0295         case QVariant::Url:
0296         case QVariant::String:
0297             m_icon = findIcon(size);
0298             break;
0299         case QVariant::Brush:
0300             // todo: fill here too?
0301         case QVariant::Color:
0302             m_icon = QImage(size, QImage::Format_Alpha8);
0303             m_icon.fill(m_source.value<QColor>());
0304             break;
0305         default:
0306             break;
0307         }
0308 
0309         if (m_icon.isNull()) {
0310             m_icon = QImage(size, QImage::Format_Alpha8);
0311             m_icon.fill(Qt::transparent);
0312         }
0313 
0314         const QColor tintColor = //
0315             !m_color.isValid() || m_color == Qt::transparent //
0316             ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor())
0317             : m_color;
0318 
0319         // TODO: initialize m_isMask with icon.isMask()
0320         if (tintColor.alpha() > 0 && (isMask() || guessMonochrome(m_icon))) {
0321             QPainter p(&m_icon);
0322             p.setCompositionMode(QPainter::CompositionMode_SourceIn);
0323             p.fillRect(m_icon.rect(), tintColor);
0324             p.end();
0325         }
0326     }
0327     m_changed = true;
0328     updatePaintedGeometry();
0329     update();
0330 }
0331 
0332 QImage Icon::findIcon(const QSize &size)
0333 {
0334     QImage img;
0335     QString iconSource = m_source.toString();
0336 
0337     if (iconSource.startsWith(QLatin1String("image://"))) {
0338         const auto multiplier = QCoreApplication::instance()->testAttribute(Qt::AA_UseHighDpiPixmaps)
0339             ? (window() ? window()->effectiveDevicePixelRatio() : qGuiApp->devicePixelRatio())
0340             : 1;
0341         QUrl iconUrl(iconSource);
0342         QString iconProviderId = iconUrl.host();
0343         // QUrl path has the  "/" prefix while iconId does not
0344         QString iconId = iconUrl.path().remove(0, 1);
0345 
0346         QSize actualSize;
0347         QQuickImageProvider *imageProvider = dynamic_cast<QQuickImageProvider *>(qmlEngine(this)->imageProvider(iconProviderId));
0348         if (!imageProvider) {
0349             return img;
0350         }
0351         switch (imageProvider->imageType()) {
0352         case QQmlImageProviderBase::Image:
0353             img = imageProvider->requestImage(iconId, &actualSize, size * multiplier);
0354             if (!img.isNull()) {
0355                 setStatus(Ready);
0356             }
0357             break;
0358         case QQmlImageProviderBase::Pixmap:
0359             img = imageProvider->requestPixmap(iconId, &actualSize, size * multiplier).toImage();
0360             if (!img.isNull()) {
0361                 setStatus(Ready);
0362             }
0363             break;
0364         case QQmlImageProviderBase::ImageResponse: {
0365             if (!m_loadedImage.isNull()) {
0366                 setStatus(Ready);
0367                 return m_loadedImage.scaled(size, Qt::KeepAspectRatio, smooth() ? Qt::SmoothTransformation : Qt::FastTransformation);
0368             }
0369             QQuickAsyncImageProvider *provider = dynamic_cast<QQuickAsyncImageProvider *>(imageProvider);
0370             auto response = provider->requestImageResponse(iconId, size * multiplier);
0371             connect(response, &QQuickImageResponse::finished, this, [iconId, response, this]() {
0372                 if (response->errorString().isEmpty()) {
0373                     QQuickTextureFactory *textureFactory = response->textureFactory();
0374                     if (textureFactory) {
0375                         m_loadedImage = textureFactory->image();
0376                         delete textureFactory;
0377                     }
0378                     if (m_loadedImage.isNull()) {
0379                         // broken image from data, inform the user of this with some useful broken-image thing...
0380                         const QIcon icon = QIcon::fromTheme(m_fallback);
0381                         m_loadedImage = icon.pixmap(window(), icon.actualSize(QSize(width(), height())), iconMode(), QIcon::On).toImage();
0382                         setStatus(Error);
0383                     } else {
0384                         setStatus(Ready);
0385                     }
0386                     polish();
0387                 }
0388                 response->deleteLater();
0389             });
0390             // Temporary icon while we wait for the real image to load...
0391             const QIcon icon = QIcon::fromTheme(m_placeholder);
0392             img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage();
0393             break;
0394         }
0395         case QQmlImageProviderBase::Texture: {
0396             QQuickTextureFactory *textureFactory = imageProvider->requestTexture(iconId, &actualSize, size * multiplier);
0397             if (textureFactory) {
0398                 img = textureFactory->image();
0399             }
0400             if (img.isNull()) {
0401                 // broken image from data, or the texture factory wasn't healthy, inform the user of this with some useful broken-image thing...
0402                 const QIcon icon = QIcon::fromTheme(m_fallback);
0403                 img = icon.pixmap(window(), icon.actualSize(QSize(width(), height())), iconMode(), QIcon::On).toImage();
0404                 setStatus(Error);
0405             } else {
0406                 setStatus(Ready);
0407             }
0408             break;
0409         }
0410         case QQmlImageProviderBase::Invalid:
0411             // will have to investigate this more
0412             setStatus(Error);
0413             break;
0414         }
0415     } else if (iconSource.startsWith(QLatin1String("http://")) || iconSource.startsWith(QLatin1String("https://"))) {
0416         if (!m_loadedImage.isNull()) {
0417             setStatus(Ready);
0418             return m_loadedImage.scaled(size, Qt::KeepAspectRatio, smooth() ? Qt::SmoothTransformation : Qt::FastTransformation);
0419         }
0420         const auto url = m_source.toUrl();
0421         QQmlEngine *engine = qmlEngine(this);
0422         QNetworkAccessManager *qnam;
0423         if (engine && (qnam = engine->networkAccessManager()) && (!m_networkReply || m_networkReply->url() != url)) {
0424             QNetworkRequest request(url);
0425             request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
0426             m_networkReply = qnam->get(request);
0427             connect(m_networkReply.data(), &QNetworkReply::finished, this, [this]() {
0428                 handleFinished(m_networkReply);
0429             });
0430         }
0431         // Temporary icon while we wait for the real image to load...
0432         const QIcon icon = QIcon::fromTheme(m_placeholder);
0433         img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage();
0434     } else {
0435         if (iconSource.startsWith(QLatin1String("qrc:/"))) {
0436             iconSource = iconSource.mid(3);
0437         } else if (iconSource.startsWith(QLatin1String("file:/"))) {
0438             iconSource = QUrl(iconSource).path();
0439         }
0440 
0441         QIcon icon;
0442         const bool isPath = iconSource.contains(QLatin1String("/"));
0443         if (isPath) {
0444             icon = QIcon(iconSource);
0445         } else {
0446             if (icon.isNull()) {
0447                 icon = m_theme->iconFromTheme(iconSource, m_color);
0448                 if (m_isMaskHeuristic && icon.name() != iconSource) {
0449                     updateIsMaskHeuristic(icon.name());
0450                     if (!m_isMaskHeuristic) {
0451                         Q_EMIT isMaskChanged();
0452                     }
0453                 }
0454             }
0455         }
0456         if (!icon.isNull()) {
0457             img = icon.pixmap(window(), icon.actualSize(window(), size), iconMode(), QIcon::On).toImage();
0458 
0459             setStatus(Ready);
0460             /*const QColor tintColor = !m_color.isValid() || m_color == Qt::transparent ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor())
0461             : m_color;
0462 
0463             if (m_isMask || icon.isMask() || iconSource.endsWith(QLatin1String("-symbolic")) || iconSource.endsWith(QLatin1String("-symbolic-rtl")) ||
0464             iconSource.endsWith(QLatin1String("-symbolic-ltr")) || guessMonochrome(img)) { //
0465                 QPainter p(&img);
0466                 p.setCompositionMode(QPainter::CompositionMode_SourceIn);
0467                 p.fillRect(img.rect(), tintColor);
0468                 p.end();
0469             }*/
0470         }
0471     }
0472 
0473     if (!iconSource.isEmpty() && img.isNull()) {
0474         setStatus(Error);
0475         const QIcon icon = QIcon::fromTheme(m_fallback);
0476         img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage();
0477     }
0478     return img;
0479 }
0480 
0481 QIcon::Mode Icon::iconMode() const
0482 {
0483     if (!isEnabled()) {
0484         return QIcon::Disabled;
0485     } else if (m_selected) {
0486         return QIcon::Selected;
0487     } else if (m_active) {
0488         return QIcon::Active;
0489     }
0490     return QIcon::Normal;
0491 }
0492 
0493 bool Icon::guessMonochrome(const QImage &img)
0494 {
0495     // don't try for too big images
0496     if (img.width() >= 256 || m_theme->supportsIconColoring()) {
0497         return false;
0498     }
0499     // round size to a standard size. hardcode as we can't use KIconLoader
0500     int stdSize;
0501     if (img.width() <= 16) {
0502         stdSize = 16;
0503     } else if (img.width() <= 22) {
0504         stdSize = 22;
0505     } else if (img.width() <= 24) {
0506         stdSize = 24;
0507     } else if (img.width() <= 32) {
0508         stdSize = 32;
0509     } else if (img.width() <= 48) {
0510         stdSize = 48;
0511     } else if (img.width() <= 64) {
0512         stdSize = 64;
0513     } else {
0514         stdSize = 128;
0515     }
0516 
0517     auto findIt = m_monochromeHeuristics.constFind(stdSize);
0518     if (findIt != m_monochromeHeuristics.constEnd()) {
0519         return findIt.value();
0520     }
0521 
0522     QHash<int, int> dist;
0523     int transparentPixels = 0;
0524     int saturatedPixels = 0;
0525     for (int x = 0; x < img.width(); x++) {
0526         for (int y = 0; y < img.height(); y++) {
0527             QColor color = QColor::fromRgba(qUnpremultiply(img.pixel(x, y)));
0528             if (color.alpha() < 100) {
0529                 ++transparentPixels;
0530                 continue;
0531             } else if (color.saturation() > 84) {
0532                 ++saturatedPixels;
0533             }
0534             dist[qGray(color.rgb())]++;
0535         }
0536     }
0537 
0538     QMultiMap<int, int> reverseDist;
0539     auto it = dist.constBegin();
0540     qreal entropy = 0;
0541     while (it != dist.constEnd()) {
0542         reverseDist.insert(it.value(), it.key());
0543         qreal probability = qreal(it.value()) / qreal(img.size().width() * img.size().height() - transparentPixels);
0544         entropy -= probability * log(probability) / log(255);
0545         ++it;
0546     }
0547 
0548     // Arbitrarily low values of entropy and colored pixels
0549     m_monochromeHeuristics[stdSize] = saturatedPixels <= (img.size().width() * img.size().height() - transparentPixels) * 0.3 && entropy <= 0.3;
0550     return m_monochromeHeuristics[stdSize];
0551 }
0552 
0553 QString Icon::fallback() const
0554 {
0555     return m_fallback;
0556 }
0557 
0558 void Icon::setFallback(const QString &fallback)
0559 {
0560     if (m_fallback != fallback) {
0561         m_fallback = fallback;
0562         Q_EMIT fallbackChanged(fallback);
0563     }
0564 }
0565 
0566 QString Icon::placeholder() const
0567 {
0568     return m_placeholder;
0569 }
0570 
0571 void Icon::setPlaceholder(const QString &placeholder)
0572 {
0573     if (m_placeholder != placeholder) {
0574         m_placeholder = placeholder;
0575         Q_EMIT placeholderChanged(placeholder);
0576     }
0577 }
0578 
0579 void Icon::setStatus(Status status)
0580 {
0581     if (status == m_status) {
0582         return;
0583     }
0584 
0585     m_status = status;
0586     Q_EMIT statusChanged();
0587 }
0588 
0589 Icon::Status Icon::status() const
0590 {
0591     return m_status;
0592 }
0593 
0594 qreal Icon::paintedWidth() const
0595 {
0596     return m_paintedWidth;
0597 }
0598 
0599 qreal Icon::paintedHeight() const
0600 {
0601     return m_paintedHeight;
0602 }
0603 
0604 void Icon::updatePaintedGeometry()
0605 {
0606     qreal newWidth = 0.0;
0607     qreal newHeight = 0.0;
0608     if (!m_icon.width() || !m_icon.height()) {
0609         newWidth = newHeight = 0.0;
0610     } else {
0611         const qreal w = widthValid() ? width() : m_icon.size().width();
0612         const qreal widthScale = w / m_icon.size().width();
0613         const qreal h = heightValid() ? height() : m_icon.size().height();
0614         const qreal heightScale = h / m_icon.size().height();
0615         if (widthScale <= heightScale) {
0616             newWidth = w;
0617             newHeight = widthScale * m_icon.size().height();
0618         } else if (heightScale < widthScale) {
0619             newWidth = heightScale * m_icon.size().width();
0620             newHeight = h;
0621         }
0622     }
0623     if (newWidth != m_paintedWidth || newHeight != m_paintedHeight) {
0624         m_paintedWidth = newWidth;
0625         m_paintedHeight = newHeight;
0626         Q_EMIT paintedAreaChanged();
0627     }
0628 }
0629 
0630 void Icon::updateIsMaskHeuristic(const QString &iconSource)
0631 {
0632     m_isMaskHeuristic = (iconSource.endsWith(QLatin1String("-symbolic")) //
0633                          || iconSource.endsWith(QLatin1String("-symbolic-rtl")) //
0634                          || iconSource.endsWith(QLatin1String("-symbolic-ltr")));
0635 }
0636 
0637 void Icon::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
0638 {
0639     if (change == QQuickItem::ItemDevicePixelRatioHasChanged) {
0640         polish();
0641     }
0642     QQuickItem::itemChange(change, value);
0643 }
0644 
0645 #include "moc_icon.cpp"