1 | /* |
2 | SPDX-FileCopyrightText: 2010 Marco Martin <mart@kde.org> |
3 | SPDX-FileCopyrightText: 2014 David Edmundson <davidedmundson@kde.org> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-or-later |
6 | */ |
7 | |
8 | #include "svgitem.h" |
9 | |
10 | #include <QDebug> |
11 | #include <QQuickWindow> |
12 | #include <QRectF> |
13 | #include <QSGTexture> |
14 | |
15 | #include "ksvg/svg.h" |
16 | |
17 | #include "managedtexturenode.h" |
18 | |
19 | #include <Kirigami/Platform/PlatformTheme> |
20 | #include <debug_p.h> |
21 | |
22 | namespace KSvg |
23 | { |
24 | SvgItem::SvgItem(QQuickItem *parent) |
25 | : QQuickItem(parent) |
26 | , m_textureChanged(false) |
27 | { |
28 | m_svg = new KSvg::Svg(this); |
29 | setFlag(flag: QQuickItem::ItemHasContents, enabled: true); |
30 | |
31 | connect(sender: m_svg, signal: &Svg::repaintNeeded, context: this, slot: &SvgItem::updateNeeded); |
32 | connect(sender: m_svg, signal: &Svg::repaintNeeded, context: this, slot: &SvgItem::naturalSizeChanged); |
33 | connect(sender: m_svg, signal: &Svg::sizeChanged, context: this, slot: &SvgItem::naturalSizeChanged); |
34 | connect(sender: m_svg, signal: &Svg::repaintNeeded, context: this, slot: &SvgItem::elementRectChanged); |
35 | connect(sender: m_svg, signal: &Svg::sizeChanged, context: this, slot: &SvgItem::elementRectChanged); |
36 | } |
37 | |
38 | SvgItem::~SvgItem() |
39 | { |
40 | // Make sure to not call anything on m_svg when this is shutting down |
41 | // Kirigami::PlatformTheme will lose its window at that point so will |
42 | // emit colorschanged, which we shouldn't react to during destructor |
43 | disconnect(sender: m_kirigamiTheme, signal: nullptr, receiver: this, member: nullptr); |
44 | } |
45 | |
46 | void SvgItem::componentComplete() |
47 | { |
48 | m_kirigamiTheme = qobject_cast<Kirigami::Platform::PlatformTheme *>(object: qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(obj: this, create: true)); |
49 | if (!m_kirigamiTheme) { |
50 | qCWarning(LOG_KSVGQML) << "No theme!" << qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(obj: this, create: true) << this; |
51 | return; |
52 | } |
53 | |
54 | auto checkApplyTheme = [this]() { |
55 | if (!m_svg->imageSet()->filePath(QStringLiteral("colors" )).isEmpty()) { |
56 | m_svg->clearColorOverrides(); |
57 | } |
58 | }; |
59 | auto applyTheme = [this]() { |
60 | if (!m_svg) { |
61 | return; |
62 | } |
63 | if (!m_svg->imageSet()->filePath(QStringLiteral("colors" )).isEmpty()) { |
64 | m_svg->clearColorOverrides(); |
65 | return; |
66 | } |
67 | m_svg->setColors({ |
68 | {Svg::Text, m_kirigamiTheme->textColor()}, |
69 | {Svg::Background, m_kirigamiTheme->backgroundColor()}, |
70 | {Svg::Highlight, m_kirigamiTheme->highlightColor()}, |
71 | {Svg::HighlightedText, m_kirigamiTheme->highlightedTextColor()}, |
72 | {Svg::PositiveText, m_kirigamiTheme->positiveTextColor()}, |
73 | {Svg::NeutralText, m_kirigamiTheme->neutralTextColor()}, |
74 | {Svg::NegativeText, m_kirigamiTheme->negativeTextColor()}, |
75 | }); |
76 | }; |
77 | applyTheme(); |
78 | connect(sender: m_kirigamiTheme, signal: &Kirigami::Platform::PlatformTheme::colorsChanged, context: this, slot&: applyTheme); |
79 | connect(sender: m_svg->imageSet(), signal: &ImageSet::imageSetChanged, context: this, slot&: checkApplyTheme); |
80 | connect(sender: m_svg, signal: &Svg::imageSetChanged, context: this, slot&: checkApplyTheme); |
81 | |
82 | QQuickItem::componentComplete(); |
83 | } |
84 | |
85 | void SvgItem::setImagePath(const QString &path) |
86 | { |
87 | if (!m_svg || m_svg->imagePath() == path) { |
88 | return; |
89 | } |
90 | |
91 | updateDevicePixelRatio(); |
92 | m_svg->setImagePath(path); |
93 | |
94 | Q_EMIT imagePathChanged(); |
95 | |
96 | if (isComponentComplete()) { |
97 | update(); |
98 | } |
99 | } |
100 | |
101 | QString SvgItem::imagePath() const |
102 | { |
103 | return m_svg->imagePath(); |
104 | } |
105 | |
106 | void SvgItem::setElementId(const QString &elementID) |
107 | { |
108 | if (elementID == m_elementID) { |
109 | return; |
110 | } |
111 | |
112 | if (implicitWidth() <= 0) { |
113 | setImplicitWidth(naturalSize().width()); |
114 | } |
115 | if (implicitHeight() <= 0) { |
116 | setImplicitHeight(naturalSize().height()); |
117 | } |
118 | |
119 | m_elementID = elementID; |
120 | Q_EMIT elementIdChanged(); |
121 | Q_EMIT naturalSizeChanged(); |
122 | Q_EMIT elementRectChanged(); |
123 | |
124 | scheduleImageUpdate(); |
125 | } |
126 | |
127 | QString SvgItem::elementId() const |
128 | { |
129 | return m_elementID; |
130 | } |
131 | |
132 | void SvgItem::setSvg(KSvg::Svg *svg) |
133 | { |
134 | if (m_svg) { |
135 | disconnect(sender: m_svg.data(), signal: nullptr, receiver: this, member: nullptr); |
136 | } |
137 | m_svg = svg; |
138 | |
139 | if (svg) { |
140 | connect(sender: svg, signal: &Svg::repaintNeeded, context: this, slot: &SvgItem::updateNeeded); |
141 | connect(sender: svg, signal: &Svg::repaintNeeded, context: this, slot: &SvgItem::naturalSizeChanged); |
142 | connect(sender: svg, signal: &Svg::repaintNeeded, context: this, slot: &SvgItem::elementRectChanged); |
143 | connect(sender: svg, signal: &Svg::sizeChanged, context: this, slot: &SvgItem::naturalSizeChanged); |
144 | connect(sender: svg, signal: &Svg::sizeChanged, context: this, slot: &SvgItem::elementRectChanged); |
145 | } |
146 | |
147 | if (implicitWidth() <= 0) { |
148 | setImplicitWidth(naturalSize().width()); |
149 | } |
150 | if (implicitHeight() <= 0) { |
151 | setImplicitHeight(naturalSize().height()); |
152 | } |
153 | |
154 | scheduleImageUpdate(); |
155 | |
156 | Q_EMIT svgChanged(); |
157 | Q_EMIT naturalSizeChanged(); |
158 | Q_EMIT elementRectChanged(); |
159 | Q_EMIT imagePathChanged(); |
160 | } |
161 | |
162 | KSvg::Svg *SvgItem::svg() const |
163 | { |
164 | return m_svg.data(); |
165 | } |
166 | |
167 | QSizeF SvgItem::naturalSize() const |
168 | { |
169 | if (!m_svg) { |
170 | return QSizeF(); |
171 | } else if (!m_elementID.isEmpty()) { |
172 | return m_svg->elementSize(elementId: m_elementID); |
173 | } |
174 | |
175 | return m_svg->size(); |
176 | } |
177 | |
178 | QRectF SvgItem::elementRect() const |
179 | { |
180 | if (!m_svg) { |
181 | return QRectF(); |
182 | } else if (!m_elementID.isEmpty()) { |
183 | return m_svg->elementRect(elementId: m_elementID); |
184 | } |
185 | |
186 | return QRectF(QPointF(0, 0), m_svg->size()); |
187 | } |
188 | |
189 | QSGNode *SvgItem::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) |
190 | { |
191 | Q_UNUSED(updatePaintNodeData); |
192 | if (!window() || !m_svg) { |
193 | delete oldNode; |
194 | return nullptr; |
195 | } |
196 | |
197 | // this is more than just an optimization, uploading a null image to QSGAtlasTexture causes a crash |
198 | if (width() == 0.0 || height() == 0.0) { |
199 | delete oldNode; |
200 | return nullptr; |
201 | } |
202 | |
203 | ManagedTextureNode *textureNode = static_cast<ManagedTextureNode *>(oldNode); |
204 | if (!textureNode) { |
205 | textureNode = new ManagedTextureNode; |
206 | m_textureChanged = true; |
207 | } |
208 | |
209 | // TODO use a heuristic to work out when to redraw |
210 | // if !m_smooth and size is approximate simply change the textureNode.rect without |
211 | // updating the material |
212 | |
213 | if (m_textureChanged || textureNode->texture()->textureSize() != QSize(width(), height())) { |
214 | // despite having a valid size sometimes we still get a null QImage from KSvg::Svg |
215 | // loading a null texture to an atlas fatals |
216 | // Dave E fixed this in Qt in 5.3.something onwards but we need this for now |
217 | if (m_image.isNull()) { |
218 | delete textureNode; |
219 | return nullptr; |
220 | } |
221 | |
222 | QSharedPointer<QSGTexture> texture(window()->createTextureFromImage(image: m_image, options: QQuickWindow::TextureCanUseAtlas)); |
223 | textureNode->setTexture(texture); |
224 | m_textureChanged = false; |
225 | |
226 | textureNode->setRect(x: 0, y: 0, w: width(), h: height()); |
227 | } |
228 | |
229 | textureNode->setFiltering(smooth() ? QSGTexture::Linear : QSGTexture::Nearest); |
230 | |
231 | return textureNode; |
232 | } |
233 | |
234 | void SvgItem::updateNeeded() |
235 | { |
236 | if (implicitWidth() <= 0) { |
237 | setImplicitWidth(naturalSize().width()); |
238 | } |
239 | if (implicitHeight() <= 0) { |
240 | setImplicitHeight(naturalSize().height()); |
241 | } |
242 | scheduleImageUpdate(); |
243 | } |
244 | |
245 | void SvgItem::scheduleImageUpdate() |
246 | { |
247 | polish(); |
248 | update(); |
249 | } |
250 | |
251 | void SvgItem::updatePolish() |
252 | { |
253 | QQuickItem::updatePolish(); |
254 | |
255 | if (m_svg) { |
256 | // setContainsMultipleImages has to be done there since m_svg can be shared with somebody else |
257 | m_textureChanged = true; |
258 | m_svg->setContainsMultipleImages(!m_elementID.isEmpty()); |
259 | m_image = m_svg->image(size: QSize(width(), height()), elementID: m_elementID); |
260 | } |
261 | } |
262 | |
263 | void SvgItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) |
264 | { |
265 | if (newGeometry.size() != oldGeometry.size() && newGeometry.isValid()) { |
266 | scheduleImageUpdate(); |
267 | } |
268 | |
269 | QQuickItem::geometryChange(newGeometry, oldGeometry); |
270 | } |
271 | |
272 | void SvgItem::updateDevicePixelRatio() |
273 | { |
274 | const auto newDevicePixelRatio = std::max<qreal>(a: 1.0, b: (window() ? window()->devicePixelRatio() : qApp->devicePixelRatio())); |
275 | if (newDevicePixelRatio != m_svg->devicePixelRatio()) { |
276 | m_svg->setDevicePixelRatio(newDevicePixelRatio); |
277 | m_textureChanged = true; |
278 | } |
279 | } |
280 | |
281 | void SvgItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) |
282 | { |
283 | if (change == ItemSceneChange && value.window) { |
284 | updateDevicePixelRatio(); |
285 | } else if (change == QQuickItem::ItemDevicePixelRatioHasChanged) { |
286 | updateDevicePixelRatio(); |
287 | } |
288 | |
289 | QQuickItem::itemChange(change, value); |
290 | } |
291 | |
292 | } // KSvg namespace |
293 | |
294 | #include "moc_svgitem.cpp" |
295 | |