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 "scenegraph/managedtexturenode.h" |
10 | |
11 | #include "platform/platformtheme.h" |
12 | #include "platform/units.h" |
13 | |
14 | #include "loggingcategory.h" |
15 | #include <QBitmap> |
16 | #include <QDebug> |
17 | #include <QGuiApplication> |
18 | #include <QIcon> |
19 | #include <QNetworkReply> |
20 | #include <QPainter> |
21 | #include <QPropertyAnimation> |
22 | #include <QQuickImageProvider> |
23 | #include <QQuickWindow> |
24 | #include <QSGSimpleTextureNode> |
25 | #include <QSGTexture> |
26 | #include <QScreen> |
27 | #include <cstdlib> |
28 | |
29 | Q_GLOBAL_STATIC(ImageTexturesCache, s_iconImageCache) |
30 | |
31 | Icon::Icon(QQuickItem *parent) |
32 | : QQuickItem(parent) |
33 | , m_active(false) |
34 | , m_selected(false) |
35 | , m_isMask(false) |
36 | { |
37 | setFlag(flag: ItemHasContents, enabled: true); |
38 | // Using 32 because Icon used to redefine implicitWidth and implicitHeight and hardcode them to 32 |
39 | setImplicitSize(32, 32); |
40 | |
41 | connect(sender: this, signal: &QQuickItem::smoothChanged, context: this, slot: &QQuickItem::polish); |
42 | connect(sender: this, signal: &QQuickItem::enabledChanged, context: this, slot: [this]() { |
43 | m_allowNextAnimation = true; |
44 | polish(); |
45 | }); |
46 | } |
47 | |
48 | Icon::~Icon() |
49 | { |
50 | } |
51 | |
52 | void Icon::componentComplete() |
53 | { |
54 | QQuickItem::componentComplete(); |
55 | |
56 | QQmlEngine *engine = qmlEngine(this); |
57 | Q_ASSERT(engine); |
58 | m_units = engine->singletonInstance<Kirigami::Platform::Units *>(uri: "org.kde.kirigami.platform" , typeName: "Units" ); |
59 | Q_ASSERT(m_units); |
60 | m_animation = new QPropertyAnimation(this); |
61 | connect(sender: m_animation, signal: &QPropertyAnimation::valueChanged, context: this, slot: &Icon::valueChanged); |
62 | connect(sender: m_animation, signal: &QPropertyAnimation::finished, context: this, slot: [this]() { |
63 | m_oldIcon = QImage(); |
64 | m_textureChanged = true; |
65 | update(); |
66 | }); |
67 | m_animation->setTargetObject(this); |
68 | m_animation->setEasingCurve(QEasingCurve::InOutCubic); |
69 | m_animation->setDuration(m_units->longDuration()); |
70 | connect(sender: m_units, signal: &Kirigami::Platform::Units::longDurationChanged, context: m_animation, slot: [this]() { |
71 | m_animation->setDuration(m_units->longDuration()); |
72 | }); |
73 | updatePaintedGeometry(); |
74 | } |
75 | |
76 | void Icon::setSource(const QVariant &icon) |
77 | { |
78 | if (m_source == icon) { |
79 | return; |
80 | } |
81 | m_source = icon; |
82 | |
83 | if (!m_theme) { |
84 | m_theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(obj: this, create: true)); |
85 | Q_ASSERT(m_theme); |
86 | |
87 | connect(sender: m_theme, signal: &Kirigami::Platform::PlatformTheme::colorsChanged, context: this, slot: &QQuickItem::polish); |
88 | } |
89 | |
90 | if (m_networkReply) { |
91 | // if there was a network query going on, interrupt it |
92 | m_networkReply->close(); |
93 | } |
94 | m_loadedImage = QImage(); |
95 | setStatus(Loading); |
96 | |
97 | polish(); |
98 | Q_EMIT sourceChanged(); |
99 | Q_EMIT validChanged(); |
100 | } |
101 | |
102 | QVariant Icon::source() const |
103 | { |
104 | return m_source; |
105 | } |
106 | |
107 | void Icon::setActive(const bool active) |
108 | { |
109 | if (active == m_active) { |
110 | return; |
111 | } |
112 | m_active = active; |
113 | if (isComponentComplete()) { |
114 | m_allowNextAnimation = true; |
115 | } |
116 | polish(); |
117 | Q_EMIT activeChanged(); |
118 | } |
119 | |
120 | bool Icon::active() const |
121 | { |
122 | return m_active; |
123 | } |
124 | |
125 | bool Icon::valid() const |
126 | { |
127 | // TODO: should this be return m_status == Ready? |
128 | // Consider an empty URL invalid, even though isNull() will say false |
129 | if (m_source.canConvert<QUrl>() && m_source.toUrl().isEmpty()) { |
130 | return false; |
131 | } |
132 | |
133 | return !m_source.isNull(); |
134 | } |
135 | |
136 | void Icon::setSelected(const bool selected) |
137 | { |
138 | if (selected == m_selected) { |
139 | return; |
140 | } |
141 | m_selected = selected; |
142 | polish(); |
143 | Q_EMIT selectedChanged(); |
144 | } |
145 | |
146 | bool Icon::selected() const |
147 | { |
148 | return m_selected; |
149 | } |
150 | |
151 | void Icon::setIsMask(bool mask) |
152 | { |
153 | if (m_isMask == mask) { |
154 | return; |
155 | } |
156 | |
157 | m_isMask = mask; |
158 | polish(); |
159 | Q_EMIT isMaskChanged(); |
160 | } |
161 | |
162 | bool Icon::isMask() const |
163 | { |
164 | return m_isMask; |
165 | } |
166 | |
167 | void Icon::setColor(const QColor &color) |
168 | { |
169 | if (m_color == color) { |
170 | return; |
171 | } |
172 | |
173 | m_color = color; |
174 | polish(); |
175 | Q_EMIT colorChanged(); |
176 | } |
177 | |
178 | QColor Icon::color() const |
179 | { |
180 | return m_color; |
181 | } |
182 | |
183 | QSGNode *Icon::createSubtree(qreal initialOpacity) |
184 | { |
185 | auto opacityNode = new QSGOpacityNode{}; |
186 | opacityNode->setFlag(QSGNode::OwnedByParent, true); |
187 | opacityNode->setOpacity(initialOpacity); |
188 | |
189 | auto *mNode = new ManagedTextureNode; |
190 | |
191 | mNode->setTexture(s_iconImageCache->loadTexture(window: window(), image: m_icon, options: QQuickWindow::TextureCanUseAtlas)); |
192 | |
193 | opacityNode->appendChildNode(node: mNode); |
194 | |
195 | return opacityNode; |
196 | } |
197 | |
198 | void Icon::updateSubtree(QSGNode *node, qreal opacity) |
199 | { |
200 | auto opacityNode = static_cast<QSGOpacityNode *>(node); |
201 | opacityNode->setOpacity(opacity); |
202 | |
203 | auto textureNode = static_cast<ManagedTextureNode *>(opacityNode->firstChild()); |
204 | textureNode->setFiltering(smooth() ? QSGTexture::Linear : QSGTexture::Nearest); |
205 | } |
206 | |
207 | QSGNode *Icon::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData * /*data*/) |
208 | { |
209 | if (m_source.isNull() || qFuzzyIsNull(d: width()) || qFuzzyIsNull(d: height())) { |
210 | delete node; |
211 | return Q_NULLPTR; |
212 | } |
213 | |
214 | if (!node) { |
215 | node = new QSGNode{}; |
216 | } |
217 | |
218 | if (m_animation && m_animation->state() == QAbstractAnimation::Running) { |
219 | if (node->childCount() < 2) { |
220 | node->appendChildNode(node: createSubtree(initialOpacity: 0.0)); |
221 | m_textureChanged = true; |
222 | } |
223 | |
224 | // Rather than doing a perfect crossfade, first fade in the new texture |
225 | // then fade out the old texture. This is done to avoid the underlying |
226 | // color bleeding through when both textures are at ~0.5 opacity, which |
227 | // causes flickering if the two textures are very similar. |
228 | updateSubtree(node: node->firstChild(), opacity: 2.0 - m_animValue * 2.0); |
229 | updateSubtree(node: node->lastChild(), opacity: m_animValue * 2.0); |
230 | } else { |
231 | if (node->childCount() == 0) { |
232 | node->appendChildNode(node: createSubtree(initialOpacity: 1.0)); |
233 | m_textureChanged = true; |
234 | } |
235 | |
236 | if (node->childCount() > 1) { |
237 | auto toRemove = node->firstChild(); |
238 | node->removeChildNode(node: toRemove); |
239 | delete toRemove; |
240 | } |
241 | |
242 | updateSubtree(node: node->firstChild(), opacity: 1.0); |
243 | } |
244 | |
245 | if (m_textureChanged) { |
246 | auto mNode = static_cast<ManagedTextureNode *>(node->lastChild()->firstChild()); |
247 | mNode->setTexture(s_iconImageCache->loadTexture(window: window(), image: m_icon, options: QQuickWindow::TextureCanUseAtlas)); |
248 | m_textureChanged = false; |
249 | m_sizeChanged = true; |
250 | } |
251 | |
252 | if (m_sizeChanged) { |
253 | const QSizeF iconPixSize(m_icon.width() / m_devicePixelRatio, m_icon.height() / m_devicePixelRatio); |
254 | const QSizeF itemPixSize = QSizeF((size() * m_devicePixelRatio).toSize()) / m_devicePixelRatio; |
255 | QRectF nodeRect(QPoint(0, 0), itemPixSize); |
256 | |
257 | if (itemPixSize.width() != 0 && itemPixSize.height() != 0) { |
258 | if (iconPixSize != itemPixSize) { |
259 | // At this point, the image will already be scaled, but we need to output it in |
260 | // the correct aspect ratio, painted centered in the viewport. So: |
261 | QRectF destination(QPointF(0, 0), QSizeF(m_icon.size()).scaled(s: m_paintedSize, mode: Qt::KeepAspectRatio)); |
262 | destination.moveCenter(p: nodeRect.center()); |
263 | destination.moveTopLeft(p: QPointF(destination.topLeft().toPoint() * m_devicePixelRatio) / m_devicePixelRatio); |
264 | nodeRect = destination; |
265 | } |
266 | } |
267 | |
268 | // Adjust the final node on the pixel grid |
269 | QPointF globalPixelPos = mapToScene(point: nodeRect.topLeft()) * m_devicePixelRatio; |
270 | QPointF posAdjust = QPointF(globalPixelPos.x() - std::round(x: globalPixelPos.x()), globalPixelPos.y() - std::round(x: globalPixelPos.y())); |
271 | nodeRect.moveTopLeft(p: nodeRect.topLeft() - posAdjust); |
272 | |
273 | for (int i = 0; i < node->childCount(); ++i) { |
274 | auto mNode = static_cast<ManagedTextureNode *>(node->childAtIndex(i)->firstChild()); |
275 | mNode->setRect(nodeRect); |
276 | } |
277 | |
278 | m_sizeChanged = false; |
279 | } |
280 | |
281 | return node; |
282 | } |
283 | |
284 | void Icon::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) |
285 | { |
286 | QQuickItem::geometryChange(newGeometry, oldGeometry); |
287 | if (newGeometry.size() != oldGeometry.size()) { |
288 | m_sizeChanged = true; |
289 | updatePaintedGeometry(); |
290 | polish(); |
291 | } |
292 | } |
293 | |
294 | void Icon::handleRedirect(QNetworkReply *reply) |
295 | { |
296 | QNetworkAccessManager *qnam = reply->manager(); |
297 | if (reply->error() != QNetworkReply::NoError) { |
298 | return; |
299 | } |
300 | const QUrl possibleRedirectUrl = reply->attribute(code: QNetworkRequest::RedirectionTargetAttribute).toUrl(); |
301 | if (!possibleRedirectUrl.isEmpty()) { |
302 | const QUrl redirectUrl = reply->url().resolved(relative: possibleRedirectUrl); |
303 | if (redirectUrl == reply->url()) { |
304 | // no infinite redirections thank you very much |
305 | reply->deleteLater(); |
306 | return; |
307 | } |
308 | reply->deleteLater(); |
309 | QNetworkRequest request(possibleRedirectUrl); |
310 | request.setAttribute(code: QNetworkRequest::CacheLoadControlAttribute, value: QNetworkRequest::PreferCache); |
311 | m_networkReply = qnam->get(request); |
312 | connect(sender: m_networkReply.data(), signal: &QNetworkReply::finished, context: this, slot: [this]() { |
313 | handleFinished(reply: m_networkReply); |
314 | }); |
315 | } |
316 | } |
317 | |
318 | void Icon::handleFinished(QNetworkReply *reply) |
319 | { |
320 | if (!reply) { |
321 | return; |
322 | } |
323 | |
324 | reply->deleteLater(); |
325 | if (!reply->attribute(code: QNetworkRequest::RedirectionTargetAttribute).isNull()) { |
326 | handleRedirect(reply); |
327 | return; |
328 | } |
329 | |
330 | m_loadedImage = QImage(); |
331 | |
332 | const QString filename = reply->url().fileName(); |
333 | if (!m_loadedImage.load(device: reply, format: filename.mid(position: filename.indexOf(c: QLatin1Char('.'))).toLatin1().constData())) { |
334 | qCWarning(KirigamiLog) << "received broken image" << reply->url(); |
335 | |
336 | // broken image from data, inform the user of this with some useful broken-image thing... |
337 | m_loadedImage = iconPixmap(icon: QIcon::fromTheme(name: m_fallback)); |
338 | } |
339 | |
340 | polish(); |
341 | } |
342 | |
343 | void Icon::updatePolish() |
344 | { |
345 | QQuickItem::updatePolish(); |
346 | |
347 | if (window()) { |
348 | m_devicePixelRatio = window()->effectiveDevicePixelRatio(); |
349 | } |
350 | |
351 | if (m_source.isNull()) { |
352 | setStatus(Ready); |
353 | updatePaintedGeometry(); |
354 | update(); |
355 | return; |
356 | } |
357 | |
358 | const QSize itemSize(width(), height()); |
359 | if (itemSize.width() != 0 && itemSize.height() != 0) { |
360 | const QSize size = itemSize; |
361 | |
362 | if (m_animation) { |
363 | m_animation->stop(); |
364 | m_oldIcon = m_icon; |
365 | } |
366 | |
367 | switch (m_source.userType()) { |
368 | case QMetaType::QPixmap: |
369 | m_icon = m_source.value<QPixmap>().toImage(); |
370 | break; |
371 | case QMetaType::QImage: |
372 | m_icon = m_source.value<QImage>(); |
373 | break; |
374 | case QMetaType::QBitmap: |
375 | m_icon = m_source.value<QBitmap>().toImage(); |
376 | break; |
377 | case QMetaType::QIcon: { |
378 | m_icon = iconPixmap(icon: m_source.value<QIcon>()); |
379 | break; |
380 | } |
381 | case QMetaType::QUrl: |
382 | case QMetaType::QString: |
383 | m_icon = findIcon(size); |
384 | break; |
385 | case QMetaType::QBrush: |
386 | // todo: fill here too? |
387 | case QMetaType::QColor: |
388 | m_icon = QImage(size, QImage::Format_Alpha8); |
389 | m_icon.fill(color: m_source.value<QColor>()); |
390 | break; |
391 | default: |
392 | break; |
393 | } |
394 | |
395 | if (m_icon.isNull()) { |
396 | m_icon = QImage(size, QImage::Format_Alpha8); |
397 | m_icon.fill(color: Qt::transparent); |
398 | } |
399 | |
400 | const QColor tintColor = // |
401 | !m_color.isValid() || m_color == Qt::transparent // |
402 | ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor()) |
403 | : m_color; |
404 | |
405 | // TODO: initialize m_isMask with icon.isMask() |
406 | if (tintColor.alpha() > 0 && isMask()) { |
407 | QPainter p(&m_icon); |
408 | p.setCompositionMode(QPainter::CompositionMode_SourceIn); |
409 | p.fillRect(m_icon.rect(), color: tintColor); |
410 | p.end(); |
411 | } |
412 | } |
413 | |
414 | // don't animate initial setting |
415 | bool animated = (m_animated || m_allowNextAnimation) && !m_oldIcon.isNull() && !m_sizeChanged && !m_blockNextAnimation; |
416 | |
417 | if (animated && m_animation) { |
418 | m_animValue = 0.0; |
419 | m_animation->setStartValue((qreal)0); |
420 | m_animation->setEndValue((qreal)1); |
421 | m_animation->start(); |
422 | m_allowNextAnimation = false; |
423 | } else { |
424 | if (m_animation) { |
425 | m_animation->stop(); |
426 | } |
427 | m_animValue = 1.0; |
428 | m_blockNextAnimation = false; |
429 | } |
430 | m_textureChanged = true; |
431 | updatePaintedGeometry(); |
432 | update(); |
433 | } |
434 | |
435 | QImage Icon::findIcon(const QSize &size) |
436 | { |
437 | QImage img; |
438 | QString iconSource = m_source.toString(); |
439 | |
440 | if (iconSource.startsWith(s: QLatin1String("image://" ))) { |
441 | QUrl iconUrl(iconSource); |
442 | QString iconProviderId = iconUrl.host(); |
443 | // QUrl path has the "/" prefix while iconId does not |
444 | QString iconId = iconUrl.path().remove(i: 0, len: 1); |
445 | |
446 | QSize actualSize; |
447 | auto engine = qmlEngine(this); |
448 | if (!engine) { |
449 | return img; |
450 | } |
451 | QQuickImageProvider *imageProvider = dynamic_cast<QQuickImageProvider *>(engine->imageProvider(id: iconProviderId)); |
452 | if (!imageProvider) { |
453 | return img; |
454 | } |
455 | switch (imageProvider->imageType()) { |
456 | case QQmlImageProviderBase::Image: |
457 | img = imageProvider->requestImage(id: iconId, size: &actualSize, requestedSize: size); |
458 | if (!img.isNull()) { |
459 | setStatus(Ready); |
460 | } |
461 | break; |
462 | case QQmlImageProviderBase::Pixmap: |
463 | img = imageProvider->requestPixmap(id: iconId, size: &actualSize, requestedSize: size).toImage(); |
464 | if (!img.isNull()) { |
465 | setStatus(Ready); |
466 | } |
467 | break; |
468 | case QQmlImageProviderBase::ImageResponse: { |
469 | if (!m_loadedImage.isNull()) { |
470 | setStatus(Ready); |
471 | return m_loadedImage.scaled(s: size, aspectMode: Qt::KeepAspectRatio, mode: smooth() ? Qt::SmoothTransformation : Qt::FastTransformation); |
472 | } |
473 | QQuickAsyncImageProvider *provider = dynamic_cast<QQuickAsyncImageProvider *>(imageProvider); |
474 | auto response = provider->requestImageResponse(id: iconId, requestedSize: size); |
475 | connect(sender: response, signal: &QQuickImageResponse::finished, context: this, slot: [iconId, response, this]() { |
476 | if (response->errorString().isEmpty()) { |
477 | QQuickTextureFactory *textureFactory = response->textureFactory(); |
478 | if (textureFactory) { |
479 | m_loadedImage = textureFactory->image(); |
480 | delete textureFactory; |
481 | } |
482 | if (m_loadedImage.isNull()) { |
483 | // broken image from data, inform the user of this with some useful broken-image thing... |
484 | m_loadedImage = iconPixmap(icon: QIcon::fromTheme(name: m_fallback)); |
485 | setStatus(Error); |
486 | } else { |
487 | setStatus(Ready); |
488 | } |
489 | polish(); |
490 | } |
491 | response->deleteLater(); |
492 | }); |
493 | // Temporary icon while we wait for the real image to load... |
494 | img = iconPixmap(icon: QIcon::fromTheme(name: m_placeholder)); |
495 | break; |
496 | } |
497 | case QQmlImageProviderBase::Texture: { |
498 | QQuickTextureFactory *textureFactory = imageProvider->requestTexture(id: iconId, size: &actualSize, requestedSize: size); |
499 | if (textureFactory) { |
500 | img = textureFactory->image(); |
501 | } |
502 | if (img.isNull()) { |
503 | // broken image from data, or the texture factory wasn't healthy, inform the user of this with some useful broken-image thing... |
504 | img = iconPixmap(icon: QIcon::fromTheme(name: m_fallback)); |
505 | setStatus(Error); |
506 | } else { |
507 | setStatus(Ready); |
508 | } |
509 | break; |
510 | } |
511 | case QQmlImageProviderBase::Invalid: |
512 | // will have to investigate this more |
513 | setStatus(Error); |
514 | break; |
515 | } |
516 | } else if (iconSource.startsWith(s: QLatin1String("http://" )) || iconSource.startsWith(s: QLatin1String("https://" ))) { |
517 | if (!m_loadedImage.isNull()) { |
518 | setStatus(Ready); |
519 | return m_loadedImage.scaled(s: size, aspectMode: Qt::KeepAspectRatio, mode: smooth() ? Qt::SmoothTransformation : Qt::FastTransformation); |
520 | } |
521 | const auto url = m_source.toUrl(); |
522 | QQmlEngine *engine = qmlEngine(this); |
523 | QNetworkAccessManager *qnam; |
524 | if (engine && (qnam = engine->networkAccessManager()) && (!m_networkReply || m_networkReply->url() != url)) { |
525 | QNetworkRequest request(url); |
526 | request.setAttribute(code: QNetworkRequest::CacheLoadControlAttribute, value: QNetworkRequest::PreferCache); |
527 | m_networkReply = qnam->get(request); |
528 | connect(sender: m_networkReply.data(), signal: &QNetworkReply::finished, context: this, slot: [this]() { |
529 | handleFinished(reply: m_networkReply); |
530 | }); |
531 | } |
532 | // Temporary icon while we wait for the real image to load... |
533 | img = iconPixmap(icon: QIcon::fromTheme(name: m_placeholder)); |
534 | } else { |
535 | if (iconSource.startsWith(s: QLatin1String("qrc:/" ))) { |
536 | iconSource = iconSource.mid(position: 3); |
537 | } else if (iconSource.startsWith(s: QLatin1String("file:/" ))) { |
538 | iconSource = QUrl(iconSource).path(); |
539 | } |
540 | |
541 | QIcon icon; |
542 | const bool isPath = iconSource.contains(s: QLatin1String("/" )); |
543 | if (isPath) { |
544 | icon = QIcon(iconSource); |
545 | } else { |
546 | const QColor tintColor = |
547 | !m_color.isValid() || m_color == Qt::transparent ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor()) : m_color; |
548 | icon = m_theme->iconFromTheme(name: iconSource, customColor: tintColor); |
549 | } |
550 | if (!icon.isNull()) { |
551 | img = iconPixmap(icon); |
552 | setStatus(Ready); |
553 | } |
554 | } |
555 | |
556 | if (!iconSource.isEmpty() && img.isNull()) { |
557 | setStatus(Error); |
558 | img = iconPixmap(icon: QIcon::fromTheme(name: m_fallback)); |
559 | } |
560 | return img; |
561 | } |
562 | |
563 | QIcon::Mode Icon::iconMode() const |
564 | { |
565 | if (!isEnabled()) { |
566 | return QIcon::Disabled; |
567 | } else if (m_selected) { |
568 | return QIcon::Selected; |
569 | } else if (m_active) { |
570 | return QIcon::Active; |
571 | } |
572 | return QIcon::Normal; |
573 | } |
574 | |
575 | QString Icon::fallback() const |
576 | { |
577 | return m_fallback; |
578 | } |
579 | |
580 | void Icon::setFallback(const QString &fallback) |
581 | { |
582 | if (m_fallback != fallback) { |
583 | m_fallback = fallback; |
584 | Q_EMIT fallbackChanged(fallback); |
585 | } |
586 | } |
587 | |
588 | QString Icon::placeholder() const |
589 | { |
590 | return m_placeholder; |
591 | } |
592 | |
593 | void Icon::setPlaceholder(const QString &placeholder) |
594 | { |
595 | if (m_placeholder != placeholder) { |
596 | m_placeholder = placeholder; |
597 | Q_EMIT placeholderChanged(placeholder); |
598 | } |
599 | } |
600 | |
601 | void Icon::setStatus(Status status) |
602 | { |
603 | if (status == m_status) { |
604 | return; |
605 | } |
606 | |
607 | m_status = status; |
608 | Q_EMIT statusChanged(); |
609 | } |
610 | |
611 | Icon::Status Icon::status() const |
612 | { |
613 | return m_status; |
614 | } |
615 | |
616 | qreal Icon::paintedWidth() const |
617 | { |
618 | return std::round(x: m_paintedSize.width()); |
619 | } |
620 | |
621 | qreal Icon::paintedHeight() const |
622 | { |
623 | return std::round(x: m_paintedSize.height()); |
624 | } |
625 | |
626 | QSize Icon::iconSizeHint() const |
627 | { |
628 | if (!m_roundToIconSize) { |
629 | return QSize(width(), height()); |
630 | } else if (m_units) { |
631 | return QSize(m_units->iconSizes()->roundedIconSize(size: std::min(a: width(), b: height())), m_units->iconSizes()->roundedIconSize(size: std::min(a: width(), b: height()))); |
632 | } else { |
633 | return QSize(std::min(a: width(), b: height()), std::min(a: width(), b: height())); |
634 | } |
635 | } |
636 | |
637 | QImage Icon::iconPixmap(const QIcon &icon) const |
638 | { |
639 | const QSize actualSize = icon.actualSize(size: iconSizeHint()); |
640 | return icon.pixmap(size: actualSize, devicePixelRatio: m_devicePixelRatio, mode: iconMode(), state: QIcon::On).toImage(); |
641 | } |
642 | |
643 | void Icon::updatePaintedGeometry() |
644 | { |
645 | QSizeF newSize; |
646 | if (!m_icon.width() || !m_icon.height()) { |
647 | newSize = {0, 0}; |
648 | } else { |
649 | qreal roundedWidth = m_units ? m_units->iconSizes()->roundedIconSize(size: std::min(a: width(), b: height())) : 32; |
650 | roundedWidth = std::round(x: roundedWidth * m_devicePixelRatio) / m_devicePixelRatio; |
651 | |
652 | if (QSizeF roundedSize(roundedWidth, roundedWidth); size() == roundedSize) { |
653 | m_paintedSize = roundedSize; |
654 | m_textureChanged = true; |
655 | update(); |
656 | Q_EMIT paintedAreaChanged(); |
657 | return; |
658 | } |
659 | if (m_roundToIconSize && m_units) { |
660 | if (m_icon.width() > m_icon.height()) { |
661 | newSize = QSizeF(roundedWidth, m_icon.height() * (roundedWidth / static_cast<qreal>(m_icon.width()))); |
662 | } else { |
663 | newSize = QSizeF(roundedWidth, roundedWidth); |
664 | } |
665 | } else { |
666 | const QSizeF iconPixSize(m_icon.width() / m_devicePixelRatio, m_icon.height() / m_devicePixelRatio); |
667 | |
668 | const qreal w = widthValid() ? width() : iconPixSize.width(); |
669 | const qreal widthScale = w / iconPixSize.width(); |
670 | const qreal h = heightValid() ? height() : iconPixSize.height(); |
671 | const qreal heightScale = h / iconPixSize.height(); |
672 | |
673 | if (widthScale <= heightScale) { |
674 | newSize = QSizeF(w, widthScale * iconPixSize.height()); |
675 | } else if (heightScale < widthScale) { |
676 | newSize = QSizeF(heightScale * iconPixSize.width(), h); |
677 | } |
678 | } |
679 | } |
680 | if (newSize != m_paintedSize) { |
681 | m_paintedSize = newSize; |
682 | m_textureChanged = true; |
683 | update(); |
684 | Q_EMIT paintedAreaChanged(); |
685 | } |
686 | } |
687 | |
688 | bool Icon::isAnimated() const |
689 | { |
690 | return m_animated; |
691 | } |
692 | |
693 | void Icon::setAnimated(bool animated) |
694 | { |
695 | if (m_animated == animated) { |
696 | return; |
697 | } |
698 | |
699 | m_animated = animated; |
700 | Q_EMIT animatedChanged(); |
701 | } |
702 | |
703 | bool Icon::roundToIconSize() const |
704 | { |
705 | return m_roundToIconSize; |
706 | } |
707 | |
708 | void Icon::setRoundToIconSize(bool roundToIconSize) |
709 | { |
710 | if (m_roundToIconSize == roundToIconSize) { |
711 | return; |
712 | } |
713 | |
714 | const QSizeF oldPaintedSize = m_paintedSize; |
715 | |
716 | m_roundToIconSize = roundToIconSize; |
717 | Q_EMIT roundToIconSizeChanged(); |
718 | |
719 | updatePaintedGeometry(); |
720 | if (oldPaintedSize != m_paintedSize) { |
721 | Q_EMIT paintedAreaChanged(); |
722 | m_textureChanged = true; |
723 | update(); |
724 | } |
725 | } |
726 | |
727 | void Icon::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) |
728 | { |
729 | if (change == QQuickItem::ItemDevicePixelRatioHasChanged) { |
730 | m_blockNextAnimation = true; |
731 | if (window()) { |
732 | m_devicePixelRatio = window()->effectiveDevicePixelRatio(); |
733 | } |
734 | polish(); |
735 | } else if (change == QQuickItem::ItemSceneChange) { |
736 | if (m_window) { |
737 | disconnect(sender: m_window.data(), signal: &QWindow::visibleChanged, receiver: this, slot: &Icon::windowVisibleChanged); |
738 | } |
739 | m_window = value.window; |
740 | if (m_window) { |
741 | connect(sender: m_window.data(), signal: &QWindow::visibleChanged, context: this, slot: &Icon::windowVisibleChanged); |
742 | m_devicePixelRatio = m_window->effectiveDevicePixelRatio(); |
743 | } |
744 | } else if (change == ItemVisibleHasChanged && value.boolValue) { |
745 | m_blockNextAnimation = true; |
746 | } |
747 | QQuickItem::itemChange(change, value); |
748 | } |
749 | |
750 | void Icon::valueChanged(const QVariant &value) |
751 | { |
752 | m_animValue = value.toReal(); |
753 | update(); |
754 | } |
755 | |
756 | void Icon::windowVisibleChanged(bool visible) |
757 | { |
758 | if (visible) { |
759 | m_blockNextAnimation = true; |
760 | } |
761 | } |
762 | |
763 | #include "moc_icon.cpp" |
764 | |