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