File indexing completed on 2024-05-05 04:46:58

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 "platformtheme.h"
0010 #include "managedtexturenode.h"
0011 
0012 #include <QBitmap>
0013 #include <QDebug>
0014 #include <QGuiApplication>
0015 #include <QIcon>
0016 #include <QPainter>
0017 #include <QQuickImageProvider>
0018 #include <QQuickWindow>
0019 #include <QSGSimpleTextureNode>
0020 #include <QSGTexture>
0021 #include <QScreen>
0022 #include <QtQml>
0023 
0024 Q_GLOBAL_STATIC(ImageTexturesCache, s_iconImageCache)
0025 
0026 Icon::Icon(QQuickItem *parent)
0027     : QQuickItem(parent)
0028     , m_changed(false)
0029     , m_active(false)
0030     , m_selected(false)
0031     , m_isMask(false)
0032 {
0033     setFlag(ItemHasContents, true);
0034     // Using 32 because Icon used to redefine implicitWidth and implicitHeight and hardcode them to 32
0035     setImplicitSize(32, 32);
0036     // FIXME: not necessary anymore
0037     connect(qApp, &QGuiApplication::paletteChanged, this, &QQuickItem::polish);
0038     connect(this, &QQuickItem::enabledChanged, this, &QQuickItem::polish);
0039     connect(this, &QQuickItem::smoothChanged, this, &QQuickItem::polish);
0040 
0041 }
0042 
0043 Icon::~Icon()
0044 {
0045 }
0046 
0047 void Icon::setSource(const QVariant &icon)
0048 {
0049     if (m_source == icon) {
0050         return;
0051     }
0052     m_source = icon;
0053     m_monochromeHeuristics.clear();
0054 
0055     if (!m_theme) {
0056         m_theme = static_cast<Maui::PlatformTheme *>(qmlAttachedPropertiesObject<Maui::PlatformTheme>(this, true));
0057         Q_ASSERT(m_theme);
0058 
0059         connect(m_theme, &Maui::PlatformTheme::PlatformTheme::colorsChanged, this, &QQuickItem::polish);
0060     }
0061 
0062     if (icon.type() == QVariant::String) {
0063         const QString iconSource = icon.toString();
0064         m_isMaskHeuristic = (iconSource.endsWith(QLatin1String("-symbolic")) //
0065                              || iconSource.endsWith(QLatin1String("-symbolic-rtl")) //
0066                              || iconSource.endsWith(QLatin1String("-symbolic-ltr")));
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 void Icon::refresh()
0199 {
0200     this->polish();
0201 }
0202 
0203 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0204 void Icon::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
0205 {
0206     QQuickItem::geometryChanged(newGeometry, oldGeometry);
0207 #else
0208 void Icon::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
0209 {
0210     QQuickItem::geometryChange(newGeometry, oldGeometry);
0211 #endif
0212     if (newGeometry.size() != oldGeometry.size()) {
0213         polish();
0214     }
0215 }
0216 
0217 void Icon::handleRedirect(QNetworkReply *reply)
0218 {
0219     QNetworkAccessManager *qnam = reply->manager();
0220     if (reply->error() != QNetworkReply::NoError) {
0221         return;
0222     }
0223     const QUrl possibleRedirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
0224     if (!possibleRedirectUrl.isEmpty()) {
0225         const QUrl redirectUrl = reply->url().resolved(possibleRedirectUrl);
0226         if (redirectUrl == reply->url()) {
0227             // no infinite redirections thank you very much
0228             reply->deleteLater();
0229             return;
0230         }
0231         reply->deleteLater();
0232         QNetworkRequest request(possibleRedirectUrl);
0233         request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
0234         m_networkReply = qnam->get(request);
0235         connect(m_networkReply.data(), &QNetworkReply::finished, this, [this]() {
0236             handleFinished(m_networkReply);
0237         });
0238     }
0239 }
0240 
0241 void Icon::handleFinished(QNetworkReply *reply)
0242 {
0243     if (!reply) {
0244         return;
0245     }
0246 
0247     reply->deleteLater();
0248     if (!reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isNull()) {
0249         handleRedirect(reply);
0250         return;
0251     }
0252 
0253     m_loadedImage = QImage();
0254 
0255     const QString filename = reply->url().fileName();
0256     if (!m_loadedImage.load(reply, filename.mid(filename.indexOf(QLatin1Char('.'))).toLatin1().constData())) {
0257         qWarning() << "received broken image" << reply->url();
0258 
0259         // broken image from data, inform the user of this with some useful broken-image thing...
0260         const QIcon icon = QIcon::fromTheme(m_fallback);
0261         m_loadedImage = icon.pixmap(window(), icon.actualSize(size().toSize()), iconMode(), QIcon::On).toImage();
0262     }
0263 
0264     polish();
0265 }
0266 
0267 void Icon::updatePolish()
0268 {
0269     QQuickItem::updatePolish();
0270 
0271     if (m_source.isNull()) {
0272         setStatus(Ready);
0273         updatePaintedGeometry();
0274         update();
0275         return;
0276     }
0277 
0278     const QSize itemSize(width(), height());
0279     if (itemSize.width() != 0 && itemSize.height() != 0) {
0280         const auto multiplier = QCoreApplication::instance()->testAttribute(Qt::AA_UseHighDpiPixmaps)
0281             ? 1
0282             : (window() ? window()->effectiveDevicePixelRatio() : qGuiApp->devicePixelRatio());
0283         const QSize size = itemSize * multiplier;
0284 
0285         switch (m_source.type()) {
0286         case QVariant::Pixmap:
0287             m_icon = m_source.value<QPixmap>().toImage();
0288             break;
0289         case QVariant::Image:
0290             m_icon = m_source.value<QImage>();
0291             break;
0292         case QVariant::Bitmap:
0293             m_icon = m_source.value<QBitmap>().toImage();
0294             break;
0295         case QVariant::Icon: {
0296             const QIcon icon = m_source.value<QIcon>();
0297             m_icon = icon.pixmap(window(), icon.actualSize(itemSize), iconMode(), QIcon::On).toImage();
0298             break;
0299         }
0300         case QVariant::Url:
0301         case QVariant::String:
0302             m_icon = findIcon(size);
0303             break;
0304         case QVariant::Brush:
0305             // todo: fill here too?
0306         case QVariant::Color:
0307             m_icon = QImage(size, QImage::Format_Alpha8);
0308             m_icon.fill(m_source.value<QColor>());
0309             break;
0310         default:
0311             break;
0312         }
0313 
0314         if (m_icon.isNull()) {
0315             m_icon = QImage(size, QImage::Format_Alpha8);
0316             m_icon.fill(Qt::transparent);
0317         }
0318 
0319         const QColor tintColor = //
0320             !m_color.isValid() || m_color == Qt::transparent //
0321             ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor())
0322             : m_color;
0323 
0324         // TODO: initialize m_isMask with icon.isMask()
0325         if (tintColor.alpha() > 0 && (isMask() || guessMonochrome(m_icon))) {
0326             QPainter p(&m_icon);
0327             p.setCompositionMode(QPainter::CompositionMode_SourceIn);
0328             p.fillRect(m_icon.rect(), tintColor);
0329             p.end();
0330         }
0331     }
0332     m_changed = true;
0333     updatePaintedGeometry();
0334     update();
0335 }
0336 
0337 QImage Icon::findIcon(const QSize &size)
0338 {
0339     QImage img;
0340     QString iconSource = m_source.toString();
0341 
0342     if (iconSource.startsWith(QLatin1String("image://"))) {
0343         const auto multiplier = QCoreApplication::instance()->testAttribute(Qt::AA_UseHighDpiPixmaps)
0344             ? (window() ? window()->effectiveDevicePixelRatio() : qGuiApp->devicePixelRatio())
0345             : 1;
0346         QUrl iconUrl(iconSource);
0347         QString iconProviderId = iconUrl.host();
0348         // QUrl path has the  "/" prefix while iconId does not
0349         QString iconId = iconUrl.path().remove(0, 1);
0350 
0351         QSize actualSize;
0352         QQuickImageProvider *imageProvider = dynamic_cast<QQuickImageProvider *>(qmlEngine(this)->imageProvider(iconProviderId));
0353         if (!imageProvider) {
0354             return img;
0355         }
0356         switch (imageProvider->imageType()) {
0357         case QQmlImageProviderBase::Image:
0358             img = imageProvider->requestImage(iconId, &actualSize, size * multiplier);
0359             if (!img.isNull()) {
0360                 setStatus(Ready);
0361             }
0362             break;
0363         case QQmlImageProviderBase::Pixmap:
0364             img = imageProvider->requestPixmap(iconId, &actualSize, size * multiplier).toImage();
0365             if (!img.isNull()) {
0366                 setStatus(Ready);
0367             }
0368             break;
0369         case QQmlImageProviderBase::ImageResponse: {
0370             if (!m_loadedImage.isNull()) {
0371                 setStatus(Ready);
0372                 return m_loadedImage.scaled(size, Qt::KeepAspectRatio, smooth() ? Qt::SmoothTransformation : Qt::FastTransformation);
0373             }
0374             QQuickAsyncImageProvider *provider = dynamic_cast<QQuickAsyncImageProvider *>(imageProvider);
0375             auto response = provider->requestImageResponse(iconId, size * multiplier);
0376             connect(response, &QQuickImageResponse::finished, this, [iconId, response, this]() {
0377                 if (response->errorString().isEmpty()) {
0378                     QQuickTextureFactory *textureFactory = response->textureFactory();
0379                     if (textureFactory) {
0380                         m_loadedImage = textureFactory->image();
0381                         delete textureFactory;
0382                     }
0383                     if (m_loadedImage.isNull()) {
0384                         // broken image from data, inform the user of this with some useful broken-image thing...
0385                         const QIcon icon = QIcon::fromTheme(m_fallback);
0386                         m_loadedImage = icon.pixmap(window(), icon.actualSize(QSize(width(), height())), iconMode(), QIcon::On).toImage();
0387                         setStatus(Error);
0388                     } else {
0389                         setStatus(Ready);
0390                     }
0391                     polish();
0392                 }
0393                 response->deleteLater();
0394             });
0395             // Temporary icon while we wait for the real image to load...
0396             const QIcon icon = QIcon::fromTheme(m_placeholder);
0397             img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage();
0398             break;
0399         }
0400         case QQmlImageProviderBase::Texture: {
0401             QQuickTextureFactory *textureFactory = imageProvider->requestTexture(iconId, &actualSize, size * multiplier);
0402             if (textureFactory) {
0403                 img = textureFactory->image();
0404             }
0405             if (img.isNull()) {
0406                 // broken image from data, or the texture factory wasn't healthy, inform the user of this with some useful broken-image thing...
0407                 const QIcon icon = QIcon::fromTheme(m_fallback);
0408                 img = icon.pixmap(window(), icon.actualSize(QSize(width(), height())), iconMode(), QIcon::On).toImage();
0409                 setStatus(Error);
0410             } else {
0411                 setStatus(Ready);
0412             }
0413             break;
0414         }
0415         case QQmlImageProviderBase::Invalid:
0416             // will have to investigate this more
0417             setStatus(Error);
0418             break;
0419         }
0420     } else if (iconSource.startsWith(QLatin1String("http://")) || iconSource.startsWith(QLatin1String("https://"))) {
0421         if (!m_loadedImage.isNull()) {
0422             setStatus(Ready);
0423             return m_loadedImage.scaled(size, Qt::KeepAspectRatio, smooth() ? Qt::SmoothTransformation : Qt::FastTransformation);
0424         }
0425         const auto url = m_source.toUrl();
0426         QQmlEngine *engine = qmlEngine(this);
0427         QNetworkAccessManager *qnam;
0428         if (engine && (qnam = engine->networkAccessManager()) && (!m_networkReply || m_networkReply->url() != url)) {
0429             QNetworkRequest request(url);
0430             request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
0431             m_networkReply = qnam->get(request);
0432             connect(m_networkReply.data(), &QNetworkReply::finished, this, [this]() {
0433                 handleFinished(m_networkReply);
0434             });
0435         }
0436         // Temporary icon while we wait for the real image to load...
0437         const QIcon icon = QIcon::fromTheme(m_placeholder);
0438         img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage();
0439     } else {
0440         if (iconSource.startsWith(QLatin1String("qrc:/"))) {
0441             iconSource = iconSource.mid(3);
0442         } else if (iconSource.startsWith(QLatin1String("file:/"))) {
0443             iconSource = QUrl(iconSource).path();
0444         }
0445 
0446         QIcon icon;
0447         const bool isPath = iconSource.contains(QLatin1String("/"));
0448         if (isPath) {
0449             icon = QIcon(iconSource);
0450         } else {
0451             if (icon.isNull()) {
0452                 icon = m_theme->iconFromTheme(iconSource, m_color);
0453             }
0454         }
0455         if (!icon.isNull()) {
0456             img = icon.pixmap(window(), icon.actualSize(window(), size), iconMode(), QIcon::On).toImage();
0457 
0458             setStatus(Ready);
0459             /*const QColor tintColor = !m_color.isValid() || m_color == Qt::transparent ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor())
0460             : m_color;
0461 
0462             if (m_isMask || icon.isMask() || iconSource.endsWith(QLatin1String("-symbolic")) || iconSource.endsWith(QLatin1String("-symbolic-rtl")) ||
0463             iconSource.endsWith(QLatin1String("-symbolic-ltr")) || guessMonochrome(img)) { //
0464                 QPainter p(&img);
0465                 p.setCompositionMode(QPainter::CompositionMode_SourceIn);
0466                 p.fillRect(img.rect(), tintColor);
0467                 p.end();
0468             }*/
0469         }
0470     }
0471 
0472     if (!iconSource.isEmpty() && img.isNull()) {
0473         setStatus(Error);
0474         const QIcon icon = QIcon::fromTheme(m_fallback);
0475         img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage();
0476     }
0477     return img;
0478 }
0479 
0480 QIcon::Mode Icon::iconMode() const
0481 {
0482     if (!isEnabled()) {
0483         return QIcon::Disabled;
0484     } else if (m_selected) {
0485         return QIcon::Selected;
0486     } else if (m_active) {
0487         return QIcon::Active;
0488     }
0489     return QIcon::Normal;
0490 }
0491 
0492 bool Icon::guessMonochrome(const QImage &img)
0493 {
0494     // don't try for too big images
0495     if (img.width() >= 256 || m_theme->supportsIconColoring()) {
0496         return false;
0497     }
0498     // round size to a standard size. hardcode as we can't use KIconLoader
0499     int stdSize;
0500     if (img.width() <= 16) {
0501         stdSize = 16;
0502     } else if (img.width() <= 22) {
0503         stdSize = 22;
0504     } else if (img.width() <= 24) {
0505         stdSize = 24;
0506     } else if (img.width() <= 32) {
0507         stdSize = 32;
0508     } else if (img.width() <= 48) {
0509         stdSize = 48;
0510     } else if (img.width() <= 64) {
0511         stdSize = 64;
0512     } else {
0513         stdSize = 128;
0514     }
0515 
0516     auto findIt = m_monochromeHeuristics.constFind(stdSize);
0517     if (findIt != m_monochromeHeuristics.constEnd()) {
0518         return findIt.value();
0519     }
0520 
0521     QHash<int, int> dist;
0522     int transparentPixels = 0;
0523     int saturatedPixels = 0;
0524     for (int x = 0; x < img.width(); x++) {
0525         for (int y = 0; y < img.height(); y++) {
0526             QColor color = QColor::fromRgba(qUnpremultiply(img.pixel(x, y)));
0527             if (color.alpha() < 100) {
0528                 ++transparentPixels;
0529                 continue;
0530             } else if (color.saturation() > 84) {
0531                 ++saturatedPixels;
0532             }
0533             dist[qGray(color.rgb())]++;
0534         }
0535     }
0536 
0537     QMultiMap<int, int> reverseDist;
0538     auto it = dist.constBegin();
0539     qreal entropy = 0;
0540     while (it != dist.constEnd()) {
0541         reverseDist.insert(it.value(), it.key());
0542         qreal probability = qreal(it.value()) / qreal(img.size().width() * img.size().height() - transparentPixels);
0543         entropy -= probability * log(probability) / log(255);
0544         ++it;
0545     }
0546 
0547     // Arbitrarily low values of entropy and colored pixels
0548     m_monochromeHeuristics[stdSize] = saturatedPixels <= (img.size().width() * img.size().height() - transparentPixels) * 0.3 && entropy <= 0.3;
0549     return m_monochromeHeuristics[stdSize];
0550 }
0551 
0552 QString Icon::fallback() const
0553 {
0554     return m_fallback;
0555 }
0556 
0557 void Icon::setFallback(const QString &fallback)
0558 {
0559     if (m_fallback != fallback) {
0560         m_fallback = fallback;
0561         Q_EMIT fallbackChanged(fallback);
0562     }
0563 }
0564 
0565 QString Icon::placeholder() const
0566 {
0567     return m_placeholder;
0568 }
0569 
0570 void Icon::setPlaceholder(const QString &placeholder)
0571 {
0572     if (m_placeholder != placeholder) {
0573         m_placeholder = placeholder;
0574         Q_EMIT placeholderChanged(placeholder);
0575     }
0576 }
0577 
0578 void Icon::setStatus(Status status)
0579 {
0580     if (status == m_status) {
0581         return;
0582     }
0583 
0584     m_status = status;
0585     Q_EMIT statusChanged();
0586 }
0587 
0588 Icon::Status Icon::status() const
0589 {
0590     return m_status;
0591 }
0592 
0593 qreal Icon::paintedWidth() const
0594 {
0595     return m_paintedWidth;
0596 }
0597 
0598 qreal Icon::paintedHeight() const
0599 {
0600     return m_paintedHeight;
0601 }
0602 
0603 void Icon::updatePaintedGeometry()
0604 {
0605     qreal newWidth = 0.0;
0606     qreal newHeight = 0.0;
0607     if (!m_icon.width() || !m_icon.height()) {
0608         newWidth = newHeight = 0.0;
0609     } else {
0610         const qreal w = widthValid() ? width() : m_icon.size().width();
0611         const qreal widthScale = w / m_icon.size().width();
0612         const qreal h = heightValid() ? height() : m_icon.size().height();
0613         const qreal heightScale = h / m_icon.size().height();
0614         if (widthScale <= heightScale) {
0615             newWidth = w;
0616             newHeight = widthScale * m_icon.size().height();
0617         } else if (heightScale < widthScale) {
0618             newWidth = heightScale * m_icon.size().width();
0619             newHeight = h;
0620         }
0621     }
0622     if (newWidth != m_paintedWidth || newHeight != m_paintedHeight) {
0623         m_paintedWidth = newWidth;
0624         m_paintedHeight = newHeight;
0625         Q_EMIT paintedAreaChanged();
0626     }
0627 }
0628 
0629 void Icon::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
0630 {
0631     if (change == QQuickItem::ItemDevicePixelRatioHasChanged) {
0632         polish();
0633     }
0634     QQuickItem::itemChange(change, value);
0635 }
0636 
0637 #include "moc_icon.cpp"