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"