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:
302 m_icon.fill(m_source.value<QColor>());
303 break;
304 default:
305 break;
306 }
307
308 if (m_icon.isNull()) {
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://"))) {
338 ? (window() ? window()->effectiveDevicePixelRatio() : qGuiApp->devicePixelRatio())
339 : 1;
340 QUrl iconUrl(iconSource);
341 QString iconProviderId = iconUrl.host();
342 // QUrl path has the "/" prefix while iconId does not
343 QString iconId = iconUrl.path().remove(0, 1);
344
346 QQuickImageProvider *imageProvider = dynamic_cast<QQuickImageProvider *>(qmlEngine(this)->imageProvider(iconProviderId));
347 if (!imageProvider) {
348 return img;
349 }
350 switch (imageProvider->imageType()) {
352 img = imageProvider->requestImage(iconId, &actualSize, size * multiplier);
353 if (!img.isNull()) {
354 setStatus(Ready);
355 }
356 break;
358 img = imageProvider->requestPixmap(iconId, &actualSize, size * multiplier).toImage();
359 if (!img.isNull()) {
360 setStatus(Ready);
361 }
362 break;
364 if (!m_loadedImage.isNull()) {
365 setStatus(Ready);
367 }
368 QQuickAsyncImageProvider *provider = dynamic_cast<QQuickAsyncImageProvider *>(imageProvider);
369 auto response = provider->requestImageResponse(iconId, size * multiplier);
370 connect(response, &QQuickImageResponse::finished, this, [iconId, response, this]() {
371 if (response->errorString().isEmpty()) {
372 QQuickTextureFactory *textureFactory = response->textureFactory();
373 if (textureFactory) {
374 m_loadedImage = textureFactory->image();
375 delete textureFactory;
376 }
377 if (m_loadedImage.isNull()) {
378 // broken image from data, inform the user of this with some useful broken-image thing...
379 const QIcon icon = QIcon::fromTheme(m_fallback);
380 m_loadedImage = icon.pixmap(window(), icon.actualSize(QSize(width(), height())), iconMode(), QIcon::On).toImage();
381 setStatus(Error);
382 } else {
383 setStatus(Ready);
384 }
385 polish();
386 }
387 response->deleteLater();
388 });
389 // Temporary icon while we wait for the real image to load...
390 const QIcon icon = QIcon::fromTheme(m_placeholder);
391 img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage();
392 break;
393 }
395 QQuickTextureFactory *textureFactory = imageProvider->requestTexture(iconId, &actualSize, size * multiplier);
396 if (textureFactory) {
397 img = textureFactory->image();
398 }
399 if (img.isNull()) {
400 // broken image from data, or the texture factory wasn't healthy, inform the user of this with some useful broken-image thing...
401 const QIcon icon = QIcon::fromTheme(m_fallback);
402 img = icon.pixmap(window(), icon.actualSize(QSize(width(), height())), iconMode(), QIcon::On).toImage();
403 setStatus(Error);
404 } else {
405 setStatus(Ready);
406 }
407 break;
408 }
410 // will have to investigate this more
411 setStatus(Error);
412 break;
413 }
414 } else if (iconSource.startsWith(QLatin1String("http://")) || iconSource.startsWith(QLatin1String("https://"))) {
415 if (!m_loadedImage.isNull()) {
416 setStatus(Ready);
417 return m_loadedImage.scaled(size, Qt::KeepAspectRatio, smooth() ? Qt::SmoothTransformation : Qt::FastTransformation);
418 }
419 const auto url = m_source.toUrl();
420 QQmlEngine *engine = qmlEngine(this);
422 if (engine && (qnam = engine->networkAccessManager()) && (!m_networkReply || m_networkReply->url() != url)) {
423 QNetworkRequest request(url);
425 m_networkReply = qnam->get(request);
426 connect(m_networkReply.data(), &QNetworkReply::finished, this, [this]() {
427 handleFinished(m_networkReply);
428 });
429 }
430 // Temporary icon while we wait for the real image to load...
431 const QIcon icon = QIcon::fromTheme(m_placeholder);
432 img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage();
433 } else {
434 if (iconSource.startsWith(QLatin1String("qrc:/"))) {
435 iconSource = iconSource.mid(3);
436 } else if (iconSource.startsWith(QLatin1String("file:/"))) {
437 iconSource = QUrl(iconSource).path();
438 }
439
440 QIcon icon;
441 const bool isPath = iconSource.contains(QLatin1String("/"));
442 if (isPath) {
443 icon = QIcon(iconSource);
444 } else {
445 if (icon.isNull()) {
446 icon = m_theme->iconFromTheme(iconSource, m_color);
447 }
448 }
449 if (!icon.isNull()) {
450 img = icon.pixmap(window(), icon.actualSize(window(), size), iconMode(), QIcon::On).toImage();
451
452 setStatus(Ready);
453 /*const QColor tintColor = !m_color.isValid() || m_color == Qt::transparent ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor())
454 : m_color;
455
456 if (m_isMask || icon.isMask() || iconSource.endsWith(QLatin1String("-symbolic")) || iconSource.endsWith(QLatin1String("-symbolic-rtl")) ||
457 iconSource.endsWith(QLatin1String("-symbolic-ltr")) || guessMonochrome(img)) { //
458 QPainter p(&img);
459 p.setCompositionMode(QPainter::CompositionMode_SourceIn);
460 p.fillRect(img.rect(), tintColor);
461 p.end();
462 }*/
463 }
464 }
465
466 if (!iconSource.isEmpty() && img.isNull()) {
467 setStatus(Error);
468 const QIcon icon = QIcon::fromTheme(m_fallback);
469 img = icon.pixmap(window(), icon.actualSize(size), iconMode(), QIcon::On).toImage();
470 }
471 return img;
472}
473
474QIcon::Mode Icon::iconMode() const
475{
476 if (!isEnabled()) {
477 return QIcon::Disabled;
478 } else if (m_selected) {
479 return QIcon::Selected;
480 } else if (m_active) {
481 return QIcon::Active;
482 }
483 return QIcon::Normal;
484}
485
486bool Icon::guessMonochrome(const QImage &img)
487{
488 // don't try for too big images
489 if (img.width() >= 256 || m_theme->supportsIconColoring()) {
490 return false;
491 }
492 // round size to a standard size. hardcode as we can't use KIconLoader
493 int stdSize;
494 if (img.width() <= 16) {
495 stdSize = 16;
496 } else if (img.width() <= 22) {
497 stdSize = 22;
498 } else if (img.width() <= 24) {
499 stdSize = 24;
500 } else if (img.width() <= 32) {
501 stdSize = 32;
502 } else if (img.width() <= 48) {
503 stdSize = 48;
504 } else if (img.width() <= 64) {
505 stdSize = 64;
506 } else {
507 stdSize = 128;
508 }
509
510 auto findIt = m_monochromeHeuristics.constFind(stdSize);
511 if (findIt != m_monochromeHeuristics.constEnd()) {
512 return findIt.value();
513 }
514
515 QHash<int, int> dist;
516 int transparentPixels = 0;
517 int saturatedPixels = 0;
518 for (int x = 0; x < img.width(); x++) {
519 for (int y = 0; y < img.height(); y++) {
520 QColor color = QColor::fromRgba(qUnpremultiply(img.pixel(x, y)));
521 if (color.alpha() < 100) {
522 ++transparentPixels;
523 continue;
524 } else if (color.saturation() > 84) {
525 ++saturatedPixels;
526 }
527 dist[qGray(color.rgb())]++;
528 }
529 }
530
531 QMultiMap<int, int> reverseDist;
532 auto it = dist.constBegin();
533 qreal entropy = 0;
534 while (it != dist.constEnd()) {
535 reverseDist.insert(it.value(), it.key());
536 qreal probability = qreal(it.value()) / qreal(img.size().width() * img.size().height() - transparentPixels);
537 entropy -= probability * log(probability) / log(255);
538 ++it;
539 }
540
541 // Arbitrarily low values of entropy and colored pixels
542 m_monochromeHeuristics[stdSize] = saturatedPixels <= (img.size().width() * img.size().height() - transparentPixels) * 0.3 && entropy <= 0.3;
543 return m_monochromeHeuristics[stdSize];
544}
545
547{
548 return m_fallback;
549}
550
551void Icon::setFallback(const QString &fallback)
552{
553 if (m_fallback != fallback) {
554 m_fallback = fallback;
555 Q_EMIT fallbackChanged(fallback);
556 }
557}
558
560{
561 return m_placeholder;
562}
563
564void Icon::setPlaceholder(const QString &placeholder)
565{
566 if (m_placeholder != placeholder) {
567 m_placeholder = placeholder;
568 Q_EMIT placeholderChanged(placeholder);
569 }
570}
571
572void Icon::setStatus(Status status)
573{
574 if (status == m_status) {
575 return;
576 }
577
578 m_status = status;
579 Q_EMIT statusChanged();
580}
581
583{
584 return m_status;
585}
586
587qreal Icon::paintedWidth() const
588{
589 return m_paintedWidth;
590}
591
592qreal Icon::paintedHeight() const
593{
594 return m_paintedHeight;
595}
596
597void Icon::updatePaintedGeometry()
598{
599 qreal newWidth = 0.0;
600 qreal newHeight = 0.0;
601 if (!m_icon.width() || !m_icon.height()) {
602 newWidth = newHeight = 0.0;
603 } else {
604 const qreal w = widthValid() ? width() : m_icon.size().width();
605 const qreal widthScale = w / m_icon.size().width();
606 const qreal h = heightValid() ? height() : m_icon.size().height();
607 const qreal heightScale = h / m_icon.size().height();
608 if (widthScale <= heightScale) {
609 newWidth = w;
610 newHeight = widthScale * m_icon.size().height();
611 } else if (heightScale < widthScale) {
612 newWidth = heightScale * m_icon.size().width();
613 newHeight = h;
614 }
615 }
616 if (newWidth != m_paintedWidth || newHeight != m_paintedHeight) {
617 m_paintedWidth = newWidth;
618 m_paintedHeight = newHeight;
619 Q_EMIT paintedAreaChanged();
620 }
621}
622
623void Icon::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
624{
626 polish();
627 }
628 QQuickItem::itemChange(change, value);
629}
630
631#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
This class is the base for color management in Maui, different platforms can reimplement this class t...
QColor highlightedTextColor
Color for text that has been highlighted, often is a light color while normal text is dark.
QColor textColor
Color for normal foregrounds, usually text, but not limited to it, anything that should be painted wi...
Q_SCRIPTABLE CaptureState status()
KGUIADDONS_EXPORT QWindow * window(QObject *job)
QAction * actualSize(const QObject *recvr, const char *slot, QObject *parent)
int alpha() const const
QColor fromRgba(QRgb rgba)
bool isValid() const const
QRgb rgb() const const
int saturation() const const
QCoreApplication * instance()
bool testAttribute(Qt::ApplicationAttribute attribute)
void paletteChanged(const QPalette &palette)
void clear()
const_iterator constBegin() const const
const_iterator constEnd() const const
const_iterator constFind(const Key &key) 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
bool load(QIODevice *device, const char *format)
QRgb pixel(const QPoint &position) const const
QImage scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) 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
T * data() 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
QSize scaled(const QSize &s, Qt::AspectRatioMode mode) 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
QString & remove(QChar ch, Qt::CaseSensitivity cs)
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
QString path(ComponentFormattingOptions options) const const
QUrl resolved(const QUrl &relative) const const
Type type() const const
bool canConvert() const const
bool isNull() const const
QString toString() const const
QUrl toUrl() const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 19 2024 12:00:22 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.