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"