1/****************************************************************************
2**
3** Copyright (C) 2017 The Qt Company Ltd.
4** Contact: http://www.qt.io/licensing/
5**
6** This file is part of the Qt Quick Controls 2 module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL3$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see http://www.qt.io/terms-conditions. For further
15** information use the contact form at http://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPLv3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or later as published by the Free
28** Software Foundation and appearing in the file LICENSE.GPL included in
29** the packaging of this file. Please review the following information to
30** ensure the GNU General Public License version 2.0 requirements will be
31** met: http://www.gnu.org/licenses/gpl-2.0.html.
32**
33** $QT_END_LICENSE$
34**
35****************************************************************************/
36
37#include "qquickninepatchimage_p.h"
38
39#include <QtCore/qfileinfo.h>
40#include <QtQuick/qsggeometry.h>
41#include <QtQuick/qsgtexturematerial.h>
42#include <QtQuick/private/qsgnode_p.h>
43#include <QtQuick/private/qquickimage_p_p.h>
44
45QT_BEGIN_NAMESPACE
46
47struct QQuickNinePatchData
48{
49 QVector<qreal> coordsForSize(qreal count) const;
50
51 inline bool isNull() const { return data.isEmpty(); }
52 inline int count() const { return data.size(); }
53 inline qreal at(int index) const { return data.at(i: index); }
54 inline qreal size() const { return data.last(); }
55
56 void fill(const QVector<qreal> &coords, qreal count);
57 void clear();
58
59private:
60 bool inverted = false;
61 QVector<qreal> data;
62};
63
64QVector<qreal> QQuickNinePatchData::coordsForSize(qreal size) const
65{
66 // n = number of stretchable sections
67 // We have to compensate when adding 0 and/or
68 // the source image width to the divs vector.
69 const int l = data.size();
70 const int n = (inverted ? l - 1 : l) / 2;
71 const qreal stretch = (size - data.last()) / n;
72
73 QVector<qreal> coords;
74 coords.reserve(asize: l);
75 coords.append(t: 0);
76
77 bool stretched = !inverted;
78 for (int i = 1; i < l; ++i) {
79 qreal advance = data[i] - data[i - 1];
80 if (stretched)
81 advance += stretch;
82 coords.append(t: coords.last() + advance);
83
84 stretched = !stretched;
85 }
86
87 return coords;
88}
89
90void QQuickNinePatchData::fill(const QVector<qreal> &coords, qreal size)
91{
92 data.clear();
93 inverted = coords.isEmpty() || coords.first() != 0;
94
95 // Reserve an extra item in case we need to add the image width/height
96 if (inverted) {
97 data.reserve(asize: coords.size() + 2);
98 data.append(t: 0);
99 } else {
100 data.reserve(asize: coords.size() + 1);
101 }
102
103 data += coords;
104 data.append(t: size);
105}
106
107void QQuickNinePatchData::clear()
108{
109 data.clear();
110}
111
112class QQuickNinePatchNode : public QSGGeometryNode
113{
114public:
115 QQuickNinePatchNode();
116 ~QQuickNinePatchNode();
117
118 void initialize(QSGTexture *texture, const QSizeF &targetSize, const QSize &sourceSize,
119 const QQuickNinePatchData &xDivs, const QQuickNinePatchData &yDivs, qreal dpr);
120
121private:
122 QSGGeometry m_geometry;
123 QSGTextureMaterial m_material;
124};
125
126QQuickNinePatchNode::QQuickNinePatchNode()
127 : m_geometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4)
128{
129 m_geometry.setDrawingMode(QSGGeometry::DrawTriangles);
130 setGeometry(&m_geometry);
131 setMaterial(&m_material);
132}
133
134QQuickNinePatchNode::~QQuickNinePatchNode()
135{
136 delete m_material.texture();
137}
138
139void QQuickNinePatchNode::initialize(QSGTexture *texture, const QSizeF &targetSize, const QSize &sourceSize,
140 const QQuickNinePatchData &xDivs, const QQuickNinePatchData &yDivs, qreal dpr)
141{
142 delete m_material.texture();
143 m_material.setTexture(texture);
144
145 const int xlen = xDivs.count();
146 const int ylen = yDivs.count();
147
148 if (xlen > 0 && ylen > 0) {
149 const int quads = (xlen - 1) * (ylen - 1);
150 static const int verticesPerQuad = 6;
151 m_geometry.allocate(vertexCount: xlen * ylen, indexCount: verticesPerQuad * quads);
152
153 QSGGeometry::TexturedPoint2D *vertices = m_geometry.vertexDataAsTexturedPoint2D();
154 QVector<qreal> xCoords = xDivs.coordsForSize(size: targetSize.width());
155 QVector<qreal> yCoords = yDivs.coordsForSize(size: targetSize.height());
156
157 for (int y = 0; y < ylen; ++y) {
158 for (int x = 0; x < xlen; ++x, ++vertices)
159 vertices->set(nx: xCoords[x] / dpr, ny: yCoords[y] / dpr,
160 ntx: xDivs.at(index: x) / sourceSize.width(),
161 nty: yDivs.at(index: y) / sourceSize.height());
162 }
163
164 quint16 *indices = m_geometry.indexDataAsUShort();
165 int n = quads;
166 for (int q = 0; n--; ++q) {
167 if ((q + 1) % xlen == 0) // next row
168 ++q;
169 // Bottom-left half quad triangle
170 indices[0] = q;
171 indices[1] = q + xlen;
172 indices[2] = q + xlen + 1;
173
174 // Top-right half quad triangle
175 indices[3] = q;
176 indices[4] = q + xlen + 1;
177 indices[5] = q + 1;
178
179 indices += verticesPerQuad;
180 }
181 }
182
183 markDirty(bits: QSGNode::DirtyGeometry | QSGNode::DirtyMaterial);
184}
185
186class QQuickNinePatchImagePrivate : public QQuickImagePrivate
187{
188 Q_DECLARE_PUBLIC(QQuickNinePatchImage)
189
190public:
191 void updatePatches();
192 void updatePaddings(const QSizeF &size, const QVector<qreal> &horizontal, const QVector<qreal> &vertical);
193 void updateInsets(const QVector<qreal> &horizontal, const QVector<qreal> &vertical);
194
195 bool resetNode = false;
196 qreal topPadding = 0;
197 qreal leftPadding = 0;
198 qreal rightPadding = 0;
199 qreal bottomPadding = 0;
200 qreal topInset = 0;
201 qreal leftInset = 0;
202 qreal rightInset = 0;
203 qreal bottomInset = 0;
204
205 QImage ninePatch;
206 QQuickNinePatchData xDivs;
207 QQuickNinePatchData yDivs;
208};
209
210static QVector<qreal> readCoords(const QRgb *data, int from, int count, int offset, QRgb color)
211{
212 int p1 = -1;
213 QVector<qreal> coords;
214 for (int i = 0; i < count; ++i) {
215 int p2 = from + i * offset;
216 if (data[p2] == color) {
217 // colored pixel
218 if (p1 == -1)
219 p1 = i;
220 } else {
221 // empty pixel
222 if (p1 != -1) {
223 coords << p1 << i;
224 p1 = -1;
225 }
226 }
227 }
228 return coords;
229}
230
231void QQuickNinePatchImagePrivate::updatePatches()
232{
233 if (ninePatch.isNull())
234 return;
235
236 int w = ninePatch.width();
237 int h = ninePatch.height();
238 const QRgb *data = reinterpret_cast<const QRgb *>(ninePatch.constBits());
239
240 const QRgb black = qRgb(r: 0,g: 0,b: 0);
241 const QRgb red = qRgb(r: 255,g: 0,b: 0);
242
243 xDivs.fill(coords: readCoords(data, from: 1, count: w - 1, offset: 1, color: black), size: w - 2); // top left -> top right
244 yDivs.fill(coords: readCoords(data, from: w, count: h - 1, offset: w, color: black), size: h - 2); // top left -> bottom left
245
246 QVector<qreal> hInsets = readCoords(data, from: (h - 1) * w + 1, count: w - 1, offset: 1, color: red); // bottom left -> bottom right
247 QVector<qreal> vInsets = readCoords(data, from: 2 * w - 1, count: h - 1, offset: w, color: red); // top right -> bottom right
248 updateInsets(horizontal: hInsets, vertical: vInsets);
249
250 const QSizeF sz(w - leftInset - rightInset, h - topInset - bottomInset);
251 QVector<qreal> hPaddings = readCoords(data, from: (h - 1) * w + leftInset + 1, count: sz.width() - 2, offset: 1, color: black); // bottom left -> bottom right
252 QVector<qreal> vPaddings = readCoords(data, from: (2 + topInset) * w - 1, count: sz.height() - 2, offset: w, color: black); // top right -> bottom right
253 updatePaddings(size: sz, horizontal: hPaddings, vertical: vPaddings);
254}
255
256void QQuickNinePatchImagePrivate::updatePaddings(const QSizeF &size, const QVector<qreal> &horizontal, const QVector<qreal> &vertical)
257{
258 Q_Q(QQuickNinePatchImage);
259 qreal oldTopPadding = topPadding;
260 qreal oldLeftPadding = leftPadding;
261 qreal oldRightPadding = rightPadding;
262 qreal oldBottomPadding = bottomPadding;
263
264 if (horizontal.count() >= 2) {
265 leftPadding = horizontal.first();
266 rightPadding = size.width() - horizontal.last() - 2;
267 } else {
268 leftPadding = 0;
269 rightPadding = 0;
270 }
271
272 if (vertical.count() >= 2) {
273 topPadding = vertical.first();
274 bottomPadding = size.height() - vertical.last() - 2;
275 } else {
276 topPadding = 0;
277 bottomPadding = 0;
278 }
279
280 if (!qFuzzyCompare(p1: oldTopPadding, p2: topPadding))
281 emit q->topPaddingChanged();
282 if (!qFuzzyCompare(p1: oldBottomPadding, p2: bottomPadding))
283 emit q->bottomPaddingChanged();
284 if (!qFuzzyCompare(p1: oldLeftPadding, p2: leftPadding))
285 emit q->leftPaddingChanged();
286 if (!qFuzzyCompare(p1: oldRightPadding, p2: rightPadding))
287 emit q->rightPaddingChanged();
288}
289
290void QQuickNinePatchImagePrivate::updateInsets(const QVector<qreal> &horizontal, const QVector<qreal> &vertical)
291{
292 Q_Q(QQuickNinePatchImage);
293 qreal oldTopInset = topInset;
294 qreal oldLeftInset = leftInset;
295 qreal oldRightInset = rightInset;
296 qreal oldBottomInset = bottomInset;
297
298 if (horizontal.count() >= 2 && horizontal.first() == 0)
299 leftInset = horizontal.at(i: 1);
300 else
301 leftInset = 0;
302
303 if (horizontal.count() == 2 && horizontal.first() > 0)
304 rightInset = horizontal.last() - horizontal.first();
305 else if (horizontal.count() == 4)
306 rightInset = horizontal.last() - horizontal.at(i: 2);
307 else
308 rightInset = 0;
309
310 if (vertical.count() >= 2 && vertical.first() == 0)
311 topInset = vertical.at(i: 1);
312 else
313 topInset = 0;
314
315 if (vertical.count() == 2 && vertical.first() > 0)
316 bottomInset = vertical.last() - vertical.first();
317 else if (vertical.count() == 4)
318 bottomInset = vertical.last() - vertical.at(i: 2);
319 else
320 bottomInset = 0;
321
322 if (!qFuzzyCompare(p1: oldTopInset, p2: topInset))
323 emit q->topInsetChanged();
324 if (!qFuzzyCompare(p1: oldBottomInset, p2: bottomInset))
325 emit q->bottomInsetChanged();
326 if (!qFuzzyCompare(p1: oldLeftInset, p2: leftInset))
327 emit q->leftInsetChanged();
328 if (!qFuzzyCompare(p1: oldRightInset, p2: rightInset))
329 emit q->rightInsetChanged();
330}
331
332QQuickNinePatchImage::QQuickNinePatchImage(QQuickItem *parent)
333 : QQuickImage(*(new QQuickNinePatchImagePrivate), parent)
334{
335}
336
337qreal QQuickNinePatchImage::topPadding() const
338{
339 Q_D(const QQuickNinePatchImage);
340 return d->topPadding / d->devicePixelRatio;
341}
342
343qreal QQuickNinePatchImage::leftPadding() const
344{
345 Q_D(const QQuickNinePatchImage);
346 return d->leftPadding / d->devicePixelRatio;
347}
348
349qreal QQuickNinePatchImage::rightPadding() const
350{
351 Q_D(const QQuickNinePatchImage);
352 return d->rightPadding / d->devicePixelRatio;
353}
354
355qreal QQuickNinePatchImage::bottomPadding() const
356{
357 Q_D(const QQuickNinePatchImage);
358 return d->bottomPadding / d->devicePixelRatio;
359}
360
361qreal QQuickNinePatchImage::topInset() const
362{
363 Q_D(const QQuickNinePatchImage);
364 return d->topInset / d->devicePixelRatio;
365}
366
367qreal QQuickNinePatchImage::leftInset() const
368{
369 Q_D(const QQuickNinePatchImage);
370 return d->leftInset / d->devicePixelRatio;
371}
372
373qreal QQuickNinePatchImage::rightInset() const
374{
375 Q_D(const QQuickNinePatchImage);
376 return d->rightInset / d->devicePixelRatio;
377}
378
379qreal QQuickNinePatchImage::bottomInset() const
380{
381 Q_D(const QQuickNinePatchImage);
382 return d->bottomInset / d->devicePixelRatio;
383}
384
385void QQuickNinePatchImage::pixmapChange()
386{
387 Q_D(QQuickNinePatchImage);
388 if (QFileInfo(d->url.fileName()).completeSuffix().toLower() == QLatin1String("9.png")) {
389 // Keep resetNode if it is already set, we do not want to miss an
390 // ImageNode->NinePatchNode change. Without this there's a chance one gets
391 // an incorrect cast on oldNode every once in a while with source changes.
392 if (!d->resetNode)
393 d->resetNode = d->ninePatch.isNull();
394
395 d->ninePatch = d->pix.image();
396 if (d->ninePatch.depth() != 32)
397 d->ninePatch = d->ninePatch.convertToFormat(f: QImage::Format_ARGB32);
398
399 int w = d->ninePatch.width();
400 int h = d->ninePatch.height();
401 d->pix.setImage(QImage(d->ninePatch.constBits() + 4 * (w + 1), w - 2, h - 2, d->ninePatch.bytesPerLine(), d->ninePatch.format()));
402
403 d->updatePatches();
404 } else {
405 /*
406 Only change resetNode when it's false; i.e. when no reset is pending.
407 updatePaintNode() will take care of setting it to false if it's true.
408
409 Consider the following changes in source:
410
411 normal.png => press.9.png => normal.png => focus.png
412
413 If the last two events happen quickly, pixmapChange() can be called
414 twice with no call to updatePaintNode() inbetween. On the first call,
415 resetNode will be true (because ninePatch is not null since it is still
416 in the process of going from a 9-patch image to a regular image),
417 and on the second call, resetNode would be false if we didn't have this check.
418 This results in the oldNode never being deleted, and QQuickImage
419 tries to static_cast a QQuickNinePatchImage to a QSGInternalImageNode.
420 */
421 if (!d->resetNode)
422 d->resetNode = !d->ninePatch.isNull();
423 d->ninePatch = QImage();
424 }
425 QQuickImage::pixmapChange();
426}
427
428QSGNode *QQuickNinePatchImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *data)
429{
430 Q_D(QQuickNinePatchImage);
431 Q_UNUSED(data);
432
433 if (d->resetNode) {
434 delete oldNode;
435 oldNode = nullptr;
436 d->resetNode = false;
437 }
438
439 QSizeF sz = size();
440 QImage image = d->pix.image();
441 if (!sz.isValid() || image.isNull()) {
442 delete oldNode;
443 return nullptr;
444 }
445
446 if (d->ninePatch.isNull())
447 return QQuickImage::updatePaintNode(oldNode, data);
448
449 QQuickNinePatchNode *patchNode = static_cast<QQuickNinePatchNode *>(oldNode);
450 if (!patchNode)
451 patchNode = new QQuickNinePatchNode;
452
453#ifdef QSG_RUNTIME_DESCRIPTION
454 qsgnode_set_description(node: patchNode, description: QString::fromLatin1(str: "QQuickNinePatchImage: '%1'").arg(a: d->url.toString()));
455#endif
456
457 // The image may wrap non-owned data (due to pixmapChange). Ensure we never
458 // pass such an image to the scenegraph, because with a separate render
459 // thread the data may become invalid (in a subsequent pixmapChange on the
460 // gui thread) by the time the renderer gets to do something with the QImage
461 // passed in here.
462 image.detach();
463
464 QSGTexture *texture = window()->createTextureFromImage(image);
465 patchNode->initialize(texture, targetSize: sz * d->devicePixelRatio, sourceSize: image.size(), xDivs: d->xDivs, yDivs: d->yDivs, dpr: d->devicePixelRatio);
466 return patchNode;
467}
468
469QT_END_NAMESPACE
470

source code of qtquickcontrols2/src/imports/controls/imagine/qquickninepatchimage.cpp