| /**************************************************************************** |
| ** |
| ** Copyright (C) 2017 The Qt Company Ltd. |
| ** Contact: http://www.qt.io/licensing/ |
| ** |
| ** This file is part of the Qt Quick Controls 2 module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL3$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see http://www.qt.io/terms-conditions. For further |
| ** information use the contact form at http://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPLv3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or later as published by the Free |
| ** Software Foundation and appearing in the file LICENSE.GPL included in |
| ** the packaging of this file. Please review the following information to |
| ** ensure the GNU General Public License version 2.0 requirements will be |
| ** met: http://www.gnu.org/licenses/gpl-2.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qquickninepatchimage_p.h" |
| |
| #include <QtCore/qfileinfo.h> |
| #include <QtQuick/qsggeometry.h> |
| #include <QtQuick/qsgtexturematerial.h> |
| #include <QtQuick/private/qsgnode_p.h> |
| #include <QtQuick/private/qquickimage_p_p.h> |
| |
| QT_BEGIN_NAMESPACE |
| |
| struct QQuickNinePatchData |
| { |
| QVector<qreal> coordsForSize(qreal count) const; |
| |
| inline bool isNull() const { return data.isEmpty(); } |
| inline int count() const { return data.size(); } |
| inline qreal at(int index) const { return data.at(index); } |
| inline qreal size() const { return data.last(); } |
| |
| void fill(const QVector<qreal> &coords, qreal count); |
| void clear(); |
| |
| private: |
| bool inverted = false; |
| QVector<qreal> data; |
| }; |
| |
| QVector<qreal> QQuickNinePatchData::coordsForSize(qreal size) const |
| { |
| // n = number of stretchable sections |
| // We have to compensate when adding 0 and/or |
| // the source image width to the divs vector. |
| const int l = data.size(); |
| const int n = (inverted ? l - 1 : l) / 2; |
| const qreal stretch = (size - data.last()) / n; |
| |
| QVector<qreal> coords; |
| coords.reserve(l); |
| coords.append(0); |
| |
| bool stretched = !inverted; |
| for (int i = 1; i < l; ++i) { |
| qreal advance = data[i] - data[i - 1]; |
| if (stretched) |
| advance += stretch; |
| coords.append(coords.last() + advance); |
| |
| stretched = !stretched; |
| } |
| |
| return coords; |
| } |
| |
| void QQuickNinePatchData::fill(const QVector<qreal> &coords, qreal size) |
| { |
| data.clear(); |
| inverted = coords.isEmpty() || coords.first() != 0; |
| |
| // Reserve an extra item in case we need to add the image width/height |
| if (inverted) { |
| data.reserve(coords.size() + 2); |
| data.append(0); |
| } else { |
| data.reserve(coords.size() + 1); |
| } |
| |
| data += coords; |
| data.append(size); |
| } |
| |
| void QQuickNinePatchData::clear() |
| { |
| data.clear(); |
| } |
| |
| class QQuickNinePatchNode : public QSGGeometryNode |
| { |
| public: |
| QQuickNinePatchNode(); |
| ~QQuickNinePatchNode(); |
| |
| void initialize(QSGTexture *texture, const QSizeF &targetSize, const QSize &sourceSize, |
| const QQuickNinePatchData &xDivs, const QQuickNinePatchData &yDivs, qreal dpr); |
| |
| private: |
| QSGGeometry m_geometry; |
| QSGTextureMaterial m_material; |
| }; |
| |
| QQuickNinePatchNode::QQuickNinePatchNode() |
| : m_geometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4) |
| { |
| m_geometry.setDrawingMode(QSGGeometry::DrawTriangles); |
| setGeometry(&m_geometry); |
| setMaterial(&m_material); |
| } |
| |
| QQuickNinePatchNode::~QQuickNinePatchNode() |
| { |
| delete m_material.texture(); |
| } |
| |
| void QQuickNinePatchNode::initialize(QSGTexture *texture, const QSizeF &targetSize, const QSize &sourceSize, |
| const QQuickNinePatchData &xDivs, const QQuickNinePatchData &yDivs, qreal dpr) |
| { |
| delete m_material.texture(); |
| m_material.setTexture(texture); |
| |
| const int xlen = xDivs.count(); |
| const int ylen = yDivs.count(); |
| |
| if (xlen > 0 && ylen > 0) { |
| const int quads = (xlen - 1) * (ylen - 1); |
| static const int verticesPerQuad = 6; |
| m_geometry.allocate(xlen * ylen, verticesPerQuad * quads); |
| |
| QSGGeometry::TexturedPoint2D *vertices = m_geometry.vertexDataAsTexturedPoint2D(); |
| QVector<qreal> xCoords = xDivs.coordsForSize(targetSize.width()); |
| QVector<qreal> yCoords = yDivs.coordsForSize(targetSize.height()); |
| |
| for (int y = 0; y < ylen; ++y) { |
| for (int x = 0; x < xlen; ++x, ++vertices) |
| vertices->set(xCoords[x] / dpr, yCoords[y] / dpr, |
| xDivs.at(x) / sourceSize.width(), |
| yDivs.at(y) / sourceSize.height()); |
| } |
| |
| quint16 *indices = m_geometry.indexDataAsUShort(); |
| int n = quads; |
| for (int q = 0; n--; ++q) { |
| if ((q + 1) % xlen == 0) // next row |
| ++q; |
| // Bottom-left half quad triangle |
| indices[0] = q; |
| indices[1] = q + xlen; |
| indices[2] = q + xlen + 1; |
| |
| // Top-right half quad triangle |
| indices[3] = q; |
| indices[4] = q + xlen + 1; |
| indices[5] = q + 1; |
| |
| indices += verticesPerQuad; |
| } |
| } |
| |
| markDirty(QSGNode::DirtyGeometry | QSGNode::DirtyMaterial); |
| } |
| |
| class QQuickNinePatchImagePrivate : public QQuickImagePrivate |
| { |
| Q_DECLARE_PUBLIC(QQuickNinePatchImage) |
| |
| public: |
| void updatePatches(); |
| void updatePaddings(const QSizeF &size, const QVector<qreal> &horizontal, const QVector<qreal> &vertical); |
| void updateInsets(const QVector<qreal> &horizontal, const QVector<qreal> &vertical); |
| |
| bool resetNode = false; |
| qreal topPadding = 0; |
| qreal leftPadding = 0; |
| qreal rightPadding = 0; |
| qreal bottomPadding = 0; |
| qreal topInset = 0; |
| qreal leftInset = 0; |
| qreal rightInset = 0; |
| qreal bottomInset = 0; |
| |
| QImage ninePatch; |
| QQuickNinePatchData xDivs; |
| QQuickNinePatchData yDivs; |
| }; |
| |
| static QVector<qreal> readCoords(const QRgb *data, int from, int count, int offset, QRgb color) |
| { |
| int p1 = -1; |
| QVector<qreal> coords; |
| for (int i = 0; i < count; ++i) { |
| int p2 = from + i * offset; |
| if (data[p2] == color) { |
| // colored pixel |
| if (p1 == -1) |
| p1 = i; |
| } else { |
| // empty pixel |
| if (p1 != -1) { |
| coords << p1 << i; |
| p1 = -1; |
| } |
| } |
| } |
| return coords; |
| } |
| |
| void QQuickNinePatchImagePrivate::updatePatches() |
| { |
| if (ninePatch.isNull()) |
| return; |
| |
| int w = ninePatch.width(); |
| int h = ninePatch.height(); |
| const QRgb *data = reinterpret_cast<const QRgb *>(ninePatch.constBits()); |
| |
| const QRgb black = qRgb(0,0,0); |
| const QRgb red = qRgb(255,0,0); |
| |
| xDivs.fill(readCoords(data, 1, w - 1, 1, black), w - 2); // top left -> top right |
| yDivs.fill(readCoords(data, w, h - 1, w, black), h - 2); // top left -> bottom left |
| |
| QVector<qreal> hInsets = readCoords(data, (h - 1) * w + 1, w - 1, 1, red); // bottom left -> bottom right |
| QVector<qreal> vInsets = readCoords(data, 2 * w - 1, h - 1, w, red); // top right -> bottom right |
| updateInsets(hInsets, vInsets); |
| |
| const QSizeF sz(w - leftInset - rightInset, h - topInset - bottomInset); |
| QVector<qreal> hPaddings = readCoords(data, (h - 1) * w + leftInset + 1, sz.width() - 2, 1, black); // bottom left -> bottom right |
| QVector<qreal> vPaddings = readCoords(data, (2 + topInset) * w - 1, sz.height() - 2, w, black); // top right -> bottom right |
| updatePaddings(sz, hPaddings, vPaddings); |
| } |
| |
| void QQuickNinePatchImagePrivate::updatePaddings(const QSizeF &size, const QVector<qreal> &horizontal, const QVector<qreal> &vertical) |
| { |
| Q_Q(QQuickNinePatchImage); |
| qreal oldTopPadding = topPadding; |
| qreal oldLeftPadding = leftPadding; |
| qreal oldRightPadding = rightPadding; |
| qreal oldBottomPadding = bottomPadding; |
| |
| if (horizontal.count() >= 2) { |
| leftPadding = horizontal.first(); |
| rightPadding = size.width() - horizontal.last() - 2; |
| } else { |
| leftPadding = 0; |
| rightPadding = 0; |
| } |
| |
| if (vertical.count() >= 2) { |
| topPadding = vertical.first(); |
| bottomPadding = size.height() - vertical.last() - 2; |
| } else { |
| topPadding = 0; |
| bottomPadding = 0; |
| } |
| |
| if (!qFuzzyCompare(oldTopPadding, topPadding)) |
| emit q->topPaddingChanged(); |
| if (!qFuzzyCompare(oldBottomPadding, bottomPadding)) |
| emit q->bottomPaddingChanged(); |
| if (!qFuzzyCompare(oldLeftPadding, leftPadding)) |
| emit q->leftPaddingChanged(); |
| if (!qFuzzyCompare(oldRightPadding, rightPadding)) |
| emit q->rightPaddingChanged(); |
| } |
| |
| void QQuickNinePatchImagePrivate::updateInsets(const QVector<qreal> &horizontal, const QVector<qreal> &vertical) |
| { |
| Q_Q(QQuickNinePatchImage); |
| qreal oldTopInset = topInset; |
| qreal oldLeftInset = leftInset; |
| qreal oldRightInset = rightInset; |
| qreal oldBottomInset = bottomInset; |
| |
| if (horizontal.count() >= 2 && horizontal.first() == 0) |
| leftInset = horizontal.at(1); |
| else |
| leftInset = 0; |
| |
| if (horizontal.count() == 2 && horizontal.first() > 0) |
| rightInset = horizontal.last() - horizontal.first(); |
| else if (horizontal.count() == 4) |
| rightInset = horizontal.last() - horizontal.at(2); |
| else |
| rightInset = 0; |
| |
| if (vertical.count() >= 2 && vertical.first() == 0) |
| topInset = vertical.at(1); |
| else |
| topInset = 0; |
| |
| if (vertical.count() == 2 && vertical.first() > 0) |
| bottomInset = vertical.last() - vertical.first(); |
| else if (vertical.count() == 4) |
| bottomInset = vertical.last() - vertical.at(2); |
| else |
| bottomInset = 0; |
| |
| if (!qFuzzyCompare(oldTopInset, topInset)) |
| emit q->topInsetChanged(); |
| if (!qFuzzyCompare(oldBottomInset, bottomInset)) |
| emit q->bottomInsetChanged(); |
| if (!qFuzzyCompare(oldLeftInset, leftInset)) |
| emit q->leftInsetChanged(); |
| if (!qFuzzyCompare(oldRightInset, rightInset)) |
| emit q->rightInsetChanged(); |
| } |
| |
| QQuickNinePatchImage::QQuickNinePatchImage(QQuickItem *parent) |
| : QQuickImage(*(new QQuickNinePatchImagePrivate), parent) |
| { |
| } |
| |
| qreal QQuickNinePatchImage::topPadding() const |
| { |
| Q_D(const QQuickNinePatchImage); |
| return d->topPadding / d->devicePixelRatio; |
| } |
| |
| qreal QQuickNinePatchImage::leftPadding() const |
| { |
| Q_D(const QQuickNinePatchImage); |
| return d->leftPadding / d->devicePixelRatio; |
| } |
| |
| qreal QQuickNinePatchImage::rightPadding() const |
| { |
| Q_D(const QQuickNinePatchImage); |
| return d->rightPadding / d->devicePixelRatio; |
| } |
| |
| qreal QQuickNinePatchImage::bottomPadding() const |
| { |
| Q_D(const QQuickNinePatchImage); |
| return d->bottomPadding / d->devicePixelRatio; |
| } |
| |
| qreal QQuickNinePatchImage::topInset() const |
| { |
| Q_D(const QQuickNinePatchImage); |
| return d->topInset / d->devicePixelRatio; |
| } |
| |
| qreal QQuickNinePatchImage::leftInset() const |
| { |
| Q_D(const QQuickNinePatchImage); |
| return d->leftInset / d->devicePixelRatio; |
| } |
| |
| qreal QQuickNinePatchImage::rightInset() const |
| { |
| Q_D(const QQuickNinePatchImage); |
| return d->rightInset / d->devicePixelRatio; |
| } |
| |
| qreal QQuickNinePatchImage::bottomInset() const |
| { |
| Q_D(const QQuickNinePatchImage); |
| return d->bottomInset / d->devicePixelRatio; |
| } |
| |
| void QQuickNinePatchImage::pixmapChange() |
| { |
| Q_D(QQuickNinePatchImage); |
| if (QFileInfo(d->url.fileName()).completeSuffix().toLower() == QLatin1String("9.png")) { |
| d->resetNode = d->ninePatch.isNull(); |
| d->ninePatch = d->pix.image(); |
| if (d->ninePatch.depth() != 32) |
| d->ninePatch = d->ninePatch.convertToFormat(QImage::Format_ARGB32); |
| |
| int w = d->ninePatch.width(); |
| int h = d->ninePatch.height(); |
| d->pix.setImage(QImage(d->ninePatch.constBits() + 4 * (w + 1), w - 2, h - 2, d->ninePatch.bytesPerLine(), d->ninePatch.format())); |
| |
| d->updatePatches(); |
| } else { |
| /* |
| Only change resetNode when it's false; i.e. when no reset is pending. |
| updatePaintNode() will take care of setting it to false if it's true. |
| |
| Consider the following changes in source: |
| |
| normal.png => press.9.png => normal.png => focus.png |
| |
| If the last two events happen quickly, pixmapChange() can be called |
| twice with no call to updatePaintNode() inbetween. On the first call, |
| resetNode will be true (because ninePatch is not null since it is still |
| in the process of going from a 9-patch image to a regular image), |
| and on the second call, resetNode would be false if we didn't have this check. |
| This results in the oldNode never being deleted, and QQuickImage |
| tries to static_cast a QQuickNinePatchImage to a QSGInternalImageNode. |
| */ |
| if (!d->resetNode) |
| d->resetNode = !d->ninePatch.isNull(); |
| d->ninePatch = QImage(); |
| } |
| QQuickImage::pixmapChange(); |
| } |
| |
| QSGNode *QQuickNinePatchImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *data) |
| { |
| Q_D(QQuickNinePatchImage); |
| Q_UNUSED(data); |
| |
| if (d->resetNode) { |
| delete oldNode; |
| oldNode = nullptr; |
| d->resetNode = false; |
| } |
| |
| QSizeF sz = size(); |
| QImage image = d->pix.image(); |
| if (!sz.isValid() || image.isNull()) { |
| delete oldNode; |
| return nullptr; |
| } |
| |
| if (d->ninePatch.isNull()) |
| return QQuickImage::updatePaintNode(oldNode, data); |
| |
| QQuickNinePatchNode *patchNode = static_cast<QQuickNinePatchNode *>(oldNode); |
| if (!patchNode) |
| patchNode = new QQuickNinePatchNode; |
| |
| #ifdef QSG_RUNTIME_DESCRIPTION |
| qsgnode_set_description(patchNode, QString::fromLatin1("QQuickNinePatchImage: '%1'").arg(d->url.toString())); |
| #endif |
| |
| QSGTexture *texture = window()->createTextureFromImage(image); |
| patchNode->initialize(texture, sz * d->devicePixelRatio, image.size(), d->xDivs, d->yDivs, d->devicePixelRatio); |
| return patchNode; |
| } |
| |
| QT_END_NAMESPACE |