MauiKit Controls

icon.cpp
1/*
2 * SPDX-FileCopyrightText: 2011 Marco Martin <mart@kde.org>
3 * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
4 *
5 * SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7
8#include "icon.h"
9#include "platformtheme.h"
10#include "managedtexturenode.h"
11
12#include <QBitmap>
13#include <QDebug>
14#include <QGuiApplication>
15#include <QIcon>
16#include <QPainter>
17#include <QQuickImageProvider>
18#include <QQuickWindow>
19#include <QSGSimpleTextureNode>
20#include <QSGTexture>
21#include <QScreen>
22#include <QtQml>
23
24Q_GLOBAL_STATIC(ImageTexturesCache, s_iconImageCache)
25
26Icon::Icon(QQuickItem *parent)
27 : QQuickItem(parent)
28 , m_changed(false)
29 , m_active(false)
30 , m_selected(false)
31 , m_isMask(false)
32{
33 setFlag(ItemHasContents, true);
34 // Using 32 because Icon used to redefine implicitWidth and implicitHeight and hardcode them to 32
35 setImplicitSize(32, 32);
36 // FIXME: not necessary anymore
40
41}
42
43Icon::~Icon()
44{
45}
46
47void Icon::setSource(const QVariant &icon)
48{
49 if (m_source == icon) {
50 return;
51 }
52 m_source = icon;
53 m_monochromeHeuristics.clear();
54
55 if (!m_theme) {
56 m_theme = static_cast<Maui::PlatformTheme *>(qmlAttachedPropertiesObject<Maui::PlatformTheme>(this, true));
57 Q_ASSERT(m_theme);
58
59 connect(m_theme, &Maui::PlatformTheme::PlatformTheme::colorsChanged, this, &QQuickItem::polish);
60 }
61
62 if (icon.type() == QVariant::String) {
63 const QString iconSource = icon.toString();
64 m_isMaskHeuristic = (iconSource.endsWith(QLatin1String("-symbolic")) //
65 || iconSource.endsWith(QLatin1String("-symbolic-rtl")) //
66 || iconSource.endsWith(QLatin1String("-symbolic-ltr")));
67 Q_EMIT isMaskChanged();
68 }
69
70 if (m_networkReply) {
71 // if there was a network query going on, interrupt it
72 m_networkReply->close();
73 }
74 m_loadedImage = QImage();
75 setStatus(Loading);
76
77 polish();
78 Q_EMIT sourceChanged();
79 Q_EMIT validChanged();
80}
81
83{
84 return m_source;
85}
86
87void Icon::setActive(const bool active)
88{
89 if (active == m_active) {
90 return;
91 }
92 m_active = active;
93 polish();
94 Q_EMIT activeChanged();
95}
96
97bool Icon::active() const
98{
99 return m_active;
100}
101
102bool Icon::valid() const
103{
104 // TODO: should this be return m_status == Ready?
105 // Consider an empty URL invalid, even though isNull() will say false
106 if (m_source.canConvert<QUrl>() && m_source.toUrl().isEmpty()) {
107 return false;
108 }
109
110 return !m_source.isNull();
111}
112
113void Icon::setSelected(const bool selected)
114{
115 if (selected == m_selected) {
116 return;
117 }
118 m_selected = selected;
119 polish();
120 Q_EMIT selectedChanged();
121}
122
123bool Icon::selected() const
124{
125 return m_selected;
126}
127
128void Icon::setIsMask(bool mask)
129{
130 if (m_isMask == mask) {
131 return;
132 }
133
134 m_isMask = mask;
135 m_isMaskHeuristic = mask;
136 polish();
137 Q_EMIT isMaskChanged();
138}
139
140bool Icon::isMask() const
141{
142 return m_isMask || m_isMaskHeuristic;
143}
144
145void Icon::setColor(const QColor &color)
146{
147 if (m_color == color) {
148 return;
149 }
150
151 m_color = color;
152 polish();
153 Q_EMIT colorChanged();
154}
155
156QColor Icon::color() const
157{
158 return m_color;
159}
160
161QSGNode *Icon::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData * /*data*/)
162{
163 if (m_source.isNull() || qFuzzyIsNull(width()) || qFuzzyIsNull(height())) {
164 delete node;
165 return Q_NULLPTR;
166 }
167
168 if (m_changed || node == nullptr) {
169 const QSize itemSize(width(), height());
170 QRect nodeRect(QPoint(0, 0), itemSize);
171
172 ManagedTextureNode *mNode = dynamic_cast<ManagedTextureNode *>(node);
173 if (!mNode) {
174 delete node;
175 mNode = new ManagedTextureNode;
176 }
177 if (itemSize.width() != 0 && itemSize.height() != 0) {
178 mNode->setTexture(s_iconImageCache->loadTexture(window(), m_icon, QQuickWindow::TextureCanUseAtlas));
179 if (m_icon.size() != itemSize) {
180 // At this point, the image will already be scaled, but we need to output it in
181 // the correct aspect ratio, painted centered in the viewport. So:
182 QRect destination(QPoint(0, 0), m_icon.size().scaled(itemSize, Qt::KeepAspectRatio));
183 destination.moveCenter(nodeRect.center());
184 nodeRect = destination;
185 }
186 }
187 mNode->setRect(nodeRect);
188 node = mNode;
189 if (smooth()) {
191 }
192 m_changed = false;
193 }
194
195 return node;
196}
197
198void Icon::refresh()
199{
200 this->polish();
201}
202
203void Icon::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
204{
205 QQuickItem::geometryChange(newGeometry, oldGeometry);
206 if (newGeometry.size() != oldGeometry.size()) {
207 polish();
208 }
209}
210
211void Icon::handleRedirect(QNetworkReply *reply)
212{
213 QNetworkAccessManager *qnam = reply->manager();
214 if (reply->error() != QNetworkReply::NoError) {
215 return;
216 }
217 const QUrl possibleRedirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
218 if (!possibleRedirectUrl.isEmpty()) {
219 const QUrl redirectUrl = reply->url().resolved(possibleRedirectUrl);
220 if (redirectUrl == reply->url()) {
221 // no infinite redirections thank you very much
222 reply->deleteLater();
223 return;
224 }
225 reply->deleteLater();
226 QNetworkRequest request(possibleRedirectUrl);
228 m_networkReply = qnam->get(request);
229 connect(m_networkReply.data(), &QNetworkReply::finished, this, [this]() {
230 handleFinished(m_networkReply);
231 });
232 }
233}
234
235void Icon::handleFinished(QNetworkReply *reply)
236{
237 if (!reply) {
238 return;
239 }
240
241 reply->deleteLater();
243 handleRedirect(reply);
244 return;
245 }
246
247 m_loadedImage = QImage();
248
249 const QString filename = reply->url().fileName();
250 if (!m_loadedImage.load(reply, filename.mid(filename.indexOf(QLatin1Char('.'))).toLatin1().constData())) {
251 qWarning() << "received broken image" << reply->url();
252
253 // broken image from data, inform the user of this with some useful broken-image thing...
254 const QIcon icon = QIcon::fromTheme(m_fallback);
255 m_loadedImage = icon.pixmap(window(), icon.actualSize(size().toSize()), iconMode(), QIcon::On).toImage();
256 }
257
258 polish();
259}
260
261void Icon::updatePolish()
262{
264
265 if (m_source.isNull()) {
266 setStatus(Ready);
267 updatePaintedGeometry();
268 update();
269 return;
270 }
271
272 const QSize itemSize(width(), height());
273 if (itemSize.width() != 0 && itemSize.height() != 0) {
275 ? 1
276 : (window() ? window()->effectiveDevicePixelRatio() : qGuiApp->devicePixelRatio());
277 const QSize size = itemSize * multiplier;
278
279 switch (m_source.type()) {
280 case QVariant::Pixmap:
281 m_icon = m_source.value<QPixmap>().toImage();
282 break;
283 case QVariant::Image:
284 m_icon = m_source.value<QImage>();
285 break;
286 case QVariant::Bitmap:
287 m_icon = m_source.value<QBitmap>().toImage();
288 break;
289 case QVariant::Icon: {
290 const QIcon icon = m_source.value<QIcon>();
291 m_icon = icon.pixmap(window(), icon.actualSize(itemSize), iconMode(), QIcon::On).toImage();
292 break;
293 }
294 case QVariant::Url:
295 case QVariant::String:
296 m_icon = findIcon(size);
297 break;
298 case QVariant::Brush:
299 // todo: fill here too?
300 case QVariant::Color:
301 m_icon = QImage(size, QImage::Format_Alpha8);
302 m_icon.fill(m_source.value<QColor>());
303 break;
304 default:
305 break;
306 }
307
308 if (m_icon.isNull()) {
309 m_icon = QImage(size, QImage::Format_Alpha8);
310 m_icon.fill(Qt::transparent);
311 }
312
313 const QColor tintColor = //
314 !m_color.isValid() || m_color == Qt::transparent //
315 ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor())
316 : m_color;
317
318 // TODO: initialize m_isMask with icon.isMask()
319 if (tintColor.alpha() > 0 && (isMask() || guessMonochrome(m_icon))) {
320 QPainter p(&m_icon);
321 p.setCompositionMode(QPainter::CompositionMode_SourceIn);
322 p.fillRect(m_icon.rect(), tintColor);
323 p.end();
324 }
325 }
326 m_changed = true;
327 updatePaintedGeometry();
328 update();
329}
330
331QImage Icon::findIcon(const QSize &size)
332{
333 QImage img;
334 QString iconSource = m_source.toString();
335
336 if (iconSource.startsWith(QLatin1String("image://"))) {
337 const auto multiplier = (window() ? window()->effectiveDevicePixelRatio() : qGuiApp->devicePixelRatio());
338
339 QUrl iconUrl(iconSource);
340 QString iconProviderId = iconUrl.host();
341 // QUrl path has the "/" prefix while iconId does not
342 QString iconId = iconUrl.path().remove(0, 1);
343
344 QSize actualSize;
345 QQuickImageProvider *imageProvider = dynamic_cast<QQuickImageProvider *>(qmlEngine(this)->imageProvider(iconProviderId));
346 if (!imageProvider) {
347 return img;
348 }
349 switch (imageProvider->imageType()) {
351 img = imageProvider->requestImage(iconId, &actualSize, size * multiplier);
352 if (!img.isNull()) {
353 setStatus(Ready);
354 }
355 break;
357 img = imageProvider->requestPixmap(iconId, &actualSize, size * multiplier).toImage();
358 if (!img.isNull()) {
359 setStatus(Ready);
360 }
361 break;
363 if (!m_loadedImage.isNull()) {
364 setStatus(Ready);
366 }
367 QQuickAsyncImageProvider *provider = dynamic_cast<QQuickAsyncImageProvider *>(imageProvider);
368 auto response = provider->requestImageResponse(iconId, size * multiplier);
369 connect(response, &QQuickImageResponse::finished, this, [iconId, response, this]() {
370 if (response->errorString().isEmpty()) {
371 QQuickTextureFactory *textureFactory = response->textureFactory();
372 if (textureFactory) {
373 m_loadedImage = textureFactory->image();
374 delete textureFactory;
375 }
376 if (m_loadedImage.isNull()) {
377 // broken image from data, inform the user of this with some useful broken-image thing...
378 const QIcon icon = QIcon::fromTheme(m_fallback);
379 m_loadedImage = icon.pixmap(window(), icon.actualSize(QSize(width(), height())), iconMode(), QIcon::On).toImage();
380 setStatus(Error);
381 } else {
382 setStatus(Ready);
383 }
384 polish();
385 }
386 response->deleteLater();
387 });
388 // Temporary icon while we wait for the real image to load...
389 const QIcon icon = QIcon::fromTheme(m_placeholder);
390 img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage();
391 break;
392 }
394 QQuickTextureFactory *textureFactory = imageProvider->requestTexture(iconId, &actualSize, size * multiplier);
395 if (textureFactory) {
396 img = textureFactory->image();
397 }
398 if (img.isNull()) {
399 // broken image from data, or the texture factory wasn't healthy, inform the user of this with some useful broken-image thing...
400 const QIcon icon = QIcon::fromTheme(m_fallback);
401 img = icon.pixmap(window(), icon.actualSize(QSize(width(), height())), iconMode(), QIcon::On).toImage();
402 setStatus(Error);
403 } else {
404 setStatus(Ready);
405 }
406 break;
407 }
409 // will have to investigate this more
410 setStatus(Error);
411 break;
412 }
413 } else if (iconSource.startsWith(QLatin1String("http://")) || iconSource.startsWith(QLatin1String("https://"))) {
414 if (!m_loadedImage.isNull()) {
415 setStatus(Ready);
417 }
418 const auto url = m_source.toUrl();
419 QQmlEngine *engine = qmlEngine(this);
420 QNetworkAccessManager *qnam;
421 if (engine && (qnam = engine->networkAccessManager()) && (!m_networkReply || m_networkReply->url() != url)) {
422 QNetworkRequest request(url);
424 m_networkReply = qnam->get(request);
425 connect(m_networkReply.data(), &QNetworkReply::finished, this, [this]() {
426 handleFinished(m_networkReply);
427 });
428 }
429 // Temporary icon while we wait for the real image to load...
430 const QIcon icon = QIcon::fromTheme(m_placeholder);
431 img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage();
432 } else {
433 if (iconSource.startsWith(QLatin1String("qrc:/"))) {
434 iconSource = iconSource.mid(3);
435 } else if (iconSource.startsWith(QLatin1String("file:/"))) {
436 iconSource = QUrl(iconSource).path();
437 }
438
439 QIcon icon;
440 const bool isPath = iconSource.contains(QLatin1String("/"));
441 if (isPath) {
442 icon = QIcon(iconSource);
443 } else {
444 if (icon.isNull()) {
445 icon = m_theme->iconFromTheme(iconSource, m_color);
446 }
447 }
448 if (!icon.isNull()) {
449 img = icon.pixmap(window(), icon.actualSize(window(), size), iconMode(), QIcon::On).toImage();
450
451 setStatus(Ready);
452 /*const QColor tintColor = !m_color.isValid() || m_color == Qt::transparent ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor())
453 : m_color;
454
455 if (m_isMask || icon.isMask() || iconSource.endsWith(QLatin1String("-symbolic")) || iconSource.endsWith(QLatin1String("-symbolic-rtl")) ||
456 iconSource.endsWith(QLatin1String("-symbolic-ltr")) || guessMonochrome(img)) { //
457 QPainter p(&img);
458 p.setCompositionMode(QPainter::CompositionMode_SourceIn);
459 p.fillRect(img.rect(), tintColor);
460 p.end();
461 }*/
462 }
463 }
464
465 if (!iconSource.isEmpty() && img.isNull()) {
466 setStatus(Error);
467 const QIcon icon = QIcon::fromTheme(m_fallback);
468 img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage();
469 }
470 return img;
471}
472
473QIcon::Mode Icon::iconMode() const
474{
475 if (!isEnabled()) {
476 return QIcon::Disabled;
477 } else if (m_selected) {
478 return QIcon::Selected;
479 } else if (m_active) {
480 return QIcon::Active;
481 }
482 return QIcon::Normal;
483}
484
485bool Icon::guessMonochrome(const QImage &img)
486{
487 // don't try for too big images
488 if (img.width() >= 256 || m_theme->supportsIconColoring()) {
489 return false;
490 }
491 // round size to a standard size. hardcode as we can't use KIconLoader
492 int stdSize;
493 if (img.width() <= 16) {
494 stdSize = 16;
495 } else if (img.width() <= 22) {
496 stdSize = 22;
497 } else if (img.width() <= 24) {
498 stdSize = 24;
499 } else if (img.width() <= 32) {
500 stdSize = 32;
501 } else if (img.width() <= 48) {
502 stdSize = 48;
503 } else if (img.width() <= 64) {
504 stdSize = 64;
505 } else {
506 stdSize = 128;
507 }
508
509 auto findIt = m_monochromeHeuristics.constFind(stdSize);
510 if (findIt != m_monochromeHeuristics.constEnd()) {
511 return findIt.value();
512 }
513
514 QHash<int, int> dist;
515 int transparentPixels = 0;
516 int saturatedPixels = 0;
517 for (int x = 0; x < img.width(); x++) {
518 for (int y = 0; y < img.height(); y++) {
519 QColor color = QColor::fromRgba(qUnpremultiply(img.pixel(x, y)));
520 if (color.alpha() < 100) {
521 ++transparentPixels;
522 continue;
523 } else if (color.saturation() > 84) {
524 ++saturatedPixels;
525 }
526 dist[qGray(color.rgb())]++;
527 }
528 }
529
530 QMultiMap<int, int> reverseDist;
531 auto it = dist.constBegin();
532 qreal entropy = 0;
533 while (it != dist.constEnd()) {
534 reverseDist.insert(it.value(), it.key());
535 qreal probability = qreal(it.value()) / qreal(img.size().width() * img.size().height() - transparentPixels);
536 entropy -= probability * log(probability) / log(255);
537 ++it;
538 }
539
540 // Arbitrarily low values of entropy and colored pixels
541 m_monochromeHeuristics[stdSize] = saturatedPixels <= (img.size().width() * img.size().height() - transparentPixels) * 0.3 && entropy <= 0.3;
542 return m_monochromeHeuristics[stdSize];
543}
544
546{
547 return m_fallback;
548}
549
550void Icon::setFallback(const QString &fallback)
551{
552 if (m_fallback != fallback) {
553 m_fallback = fallback;
554 Q_EMIT fallbackChanged(fallback);
555 }
556}
557
559{
560 return m_placeholder;
561}
562
563void Icon::setPlaceholder(const QString &placeholder)
564{
565 if (m_placeholder != placeholder) {
566 m_placeholder = placeholder;
567 Q_EMIT placeholderChanged(placeholder);
568 }
569}
570
571void Icon::setStatus(Status status)
572{
573 if (status == m_status) {
574 return;
575 }
576
577 m_status = status;
578 Q_EMIT statusChanged();
579}
580
582{
583 return m_status;
584}
585
586qreal Icon::paintedWidth() const
587{
588 return m_paintedWidth;
589}
590
591qreal Icon::paintedHeight() const
592{
593 return m_paintedHeight;
594}
595
596void Icon::updatePaintedGeometry()
597{
598 qreal newWidth = 0.0;
599 qreal newHeight = 0.0;
600 if (!m_icon.width() || !m_icon.height()) {
601 newWidth = newHeight = 0.0;
602 } else {
603 const qreal w = widthValid() ? width() : m_icon.size().width();
604 const qreal widthScale = w / m_icon.size().width();
605 const qreal h = heightValid() ? height() : m_icon.size().height();
606 const qreal heightScale = h / m_icon.size().height();
607 if (widthScale <= heightScale) {
608 newWidth = w;
609 newHeight = widthScale * m_icon.size().height();
610 } else if (heightScale < widthScale) {
611 newWidth = heightScale * m_icon.size().width();
612 newHeight = h;
613 }
614 }
615 if (newWidth != m_paintedWidth || newHeight != m_paintedHeight) {
616 m_paintedWidth = newWidth;
617 m_paintedHeight = newHeight;
618 Q_EMIT paintedAreaChanged();
619 }
620}
621
622void Icon::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
623{
625 polish();
626 }
627 QQuickItem::itemChange(change, value);
628}
629
630#include "moc_icon.cpp"
qreal paintedHeight
Definition icon.h:154
QString placeholder
Definition icon.h:79
qreal paintedWidth
Definition icon.h:146
bool selected
Definition icon.h:113
bool valid
Definition icon.h:99
QColor color
Definition icon.h:130
Icon::Status status
Definition icon.h:138
QString fallback
Definition icon.h:66
QML_ELEMENTQVariant source
bool isMask
Definition icon.h:122
bool active
Definition icon.h:94
Q_SCRIPTABLE CaptureState status()
QAction * actualSize(const QObject *recvr, const char *slot, QObject *parent)
int alpha() const const
QColor fromRgba(QRgb rgba)
QCoreApplication * instance()
bool testAttribute(Qt::ApplicationAttribute attribute)
void paletteChanged(const QPalette &palette)
const_iterator constBegin() const const
const_iterator constEnd() const const
QSize actualSize(QWindow *window, const QSize &size, Mode mode, State state) const const
QPixmap pixmap(QWindow *window, const QSize &size, Mode mode, State state) const const
QIcon fromTheme(const QString &name)
bool isNull() const const
int height() const const
bool isNull() const const
QRgb pixel(const QPoint &position) const const
QSize size() const const
int width() const const
iterator insert(const Key &key, const T &value)
QNetworkReply * get(const QNetworkRequest &request)
QVariant attribute(QNetworkRequest::Attribute code) const const
NetworkError error() const const
QNetworkAccessManager * manager() const const
QUrl url() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
CompositionMode_SourceIn
QImage toImage() const const
QNetworkAccessManager * networkAccessManager() const const
virtual QQuickImageResponse * requestImageResponse(const QString &id, const QSize &requestedSize)=0
virtual ImageType imageType() const const override
virtual QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize)
virtual QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize)
virtual QQuickTextureFactory * requestTexture(const QString &id, QSize *size, const QSize &requestedSize)
void enabledChanged()
virtual void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
bool heightValid() const const
virtual void itemChange(ItemChange change, const ItemChangeData &value)
void polish()
QSizeF size() const const
void smoothChanged(bool)
void update()
virtual void updatePolish()
bool widthValid() const const
QQuickWindow * window() const const
virtual QImage image() const const
qreal effectiveDevicePixelRatio() const const
QSizeF size() const const
void setFiltering(QSGTexture::Filtering filtering)
void setRect(const QRectF &r)
int height() const const
int width() const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString mid(qsizetype position, qsizetype n) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
AA_UseHighDpiPixmaps
KeepAspectRatio
transparent
SmoothTransformation
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QString fileName(ComponentFormattingOptions options) const const
bool isEmpty() const const
QUrl resolved(const QUrl &relative) const const
Type type() const const
void clear()
bool isNull() const const
QString toString() const const
QUrl toUrl() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Mar 21 2025 12:01:42 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.