diff options
Diffstat (limited to 'src/quicknativestyle/items/qquickstyleitem.cpp')
-rw-r--r-- | src/quicknativestyle/items/qquickstyleitem.cpp | 563 |
1 files changed, 563 insertions, 0 deletions
diff --git a/src/quicknativestyle/items/qquickstyleitem.cpp b/src/quicknativestyle/items/qquickstyleitem.cpp new file mode 100644 index 0000000000..6bc594b8b0 --- /dev/null +++ b/src/quicknativestyle/items/qquickstyleitem.cpp @@ -0,0 +1,563 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qquickstyleitem.h" + +#include <QtCore/qscopedvaluerollback.h> +#include <QtCore/qdir.h> + +#include <QtQuick/qsgninepatchnode.h> +#include <QtQuick/private/qquickwindow_p.h> +#include <QtQuick/qquickwindow.h> +#include <QtQuick/qquickrendercontrol.h> + +#include <QtQuickTemplates2/private/qquickcontrol_p.h> +#include <QtQuickTemplates2/private/qquickbutton_p.h> + +#include <QtQml/qqml.h> + +#include "qquickstyleitembutton.h" +#include "qquickstylehelper_p.h" + +QT_BEGIN_NAMESPACE + +QDebug operator<<(QDebug debug, const QQuickStyleMargins &padding) +{ + QDebugStateSaver saver(debug); + debug.nospace(); + debug << "StyleMargins("; + debug << padding.left() << ", "; + debug << padding.top() << ", "; + debug << padding.right() << ", "; + debug << padding.bottom(); + debug << ')'; + return debug; +} + +QDebug operator<<(QDebug debug, const StyleItemGeometry &cg) +{ + QDebugStateSaver saver(debug); + debug.nospace(); + debug << "StyleItemGeometry("; + debug << "implicitSize:" << cg.implicitSize << ", "; + debug << "contentRect:" << cg.contentRect << ", "; + debug << "layoutRect:" << cg.layoutRect << ", "; + debug << "minimumSize:" << cg.minimumSize << ", "; + debug << "9patchMargins:" << cg.ninePatchMargins; + debug << ')'; + return debug; +} + +int QQuickStyleItem::dprAlignedSize(const int size) const +{ + // Return the first value equal to or bigger than size + // that is a whole number when multiplied with the dpr. + static int multiplier = [&]() { + const qreal dpr = window()->effectiveDevicePixelRatio(); + for (int m = 1; m <= 10; ++m) { + const qreal v = m * dpr; + if (v == int(v)) + return m; + } + + qWarning() << "The current dpr (" << dpr << ") is not supported" + << "by the style and might result in drawing artifacts"; + return 1; + }(); + + return int(qCeil(qreal(size) / qreal(multiplier)) * multiplier); +} + +QQuickStyleItem::QQuickStyleItem(QQuickItem *parent) + : QQuickItem(parent) +{ + setFlag(QQuickItem::ItemHasContents); +} + +QQuickStyleItem::~QQuickStyleItem() +{ +} + +void QQuickStyleItem::connectToControl() const +{ + connect(m_control, &QQuickStyleItem::enabledChanged, this, &QQuickStyleItem::markImageDirty); + connect(m_control, &QQuickItem::activeFocusChanged, this, &QQuickStyleItem::markImageDirty); + + if (QQuickWindow *win = window()) { + connect(win, &QQuickWindow::activeChanged, this, &QQuickStyleItem::markImageDirty); + m_connectedWindow = win; + } +} + +void QQuickStyleItem::markImageDirty() +{ + m_dirty.setFlag(DirtyFlag::Image); + if (isComponentComplete()) + polish(); +} + +void QQuickStyleItem::markGeometryDirty() +{ + m_dirty.setFlag(DirtyFlag::Geometry); + if (isComponentComplete()) + polish(); +} + +QSGNode *QQuickStyleItem::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *) +{ + QSGNinePatchNode *node = static_cast<QSGNinePatchNode *>(oldNode); + if (!node) + node = window()->createNinePatchNode(); + + if (m_paintedImage.isNull()) { + // If we cannot create a texture, the node should not exist either + // because its material requires a texture. + delete node; + return nullptr; + } + + const auto texture = window()->createTextureFromImage(m_paintedImage, QQuickWindow::TextureCanUseAtlas); + + QRectF bounds = boundingRect(); + const qreal dpr = window()->effectiveDevicePixelRatio(); + const QSizeF unscaledImageSize = QSizeF(m_paintedImage.size()) / dpr; + + // We can scale the image up with a nine patch node, but should + // avoid to scale it down. Otherwise the nine patch image will look + // wrapped (or look truncated, in case of no padding). So if the + // item is smaller that the image, don't scale. + if (bounds.width() < unscaledImageSize.width()) + bounds.setWidth(unscaledImageSize.width()); + if (bounds.height() < unscaledImageSize.height()) + bounds.setHeight(unscaledImageSize.height()); + +#ifdef QT_DEBUG + if (m_debugFlags.testFlag(Unscaled)) { + bounds.setSize(unscaledImageSize); + qqc2Info() << "Setting qsg node size to the unscaled size of m_paintedImage:" << bounds; + } +#endif + + if (m_useNinePatchImage) { + QMargins padding = m_styleItemGeometry.ninePatchMargins; + if (padding.right() == -1) { + // Special case: a padding of -1 means that + // the image shouldn't scale in the given direction. + padding.setLeft(0); + padding.setRight(0); + } + if (padding.bottom() == -1) { + padding.setTop(0); + padding.setBottom(0); + } + node->setPadding(padding.left(), padding.top(), padding.right(), padding.bottom()); + } + + node->setBounds(bounds); + node->setTexture(texture); + node->setDevicePixelRatio(dpr); + node->update(); + + return node; +} + +QStyle::State QQuickStyleItem::controlSize(QQuickItem *item) +{ + // TODO: add proper API for small and mini + if (item->metaObject()->indexOfProperty("qqc2_style_small") != -1) + return QStyle::State_Small; + if (item->metaObject()->indexOfProperty("qqc2_style_mini") != -1) + return QStyle::State_Mini; + return QStyle::State_None; +} + +static QWindow *effectiveWindow(QQuickWindow *window) +{ + QWindow *renderWindow = QQuickRenderControl::renderWindowFor(window); + return renderWindow ? renderWindow : window; +} + +void QQuickStyleItem::initStyleOptionBase(QStyleOption &styleOption) const +{ + Q_ASSERT(m_control); + + styleOption.control = const_cast<QQuickItem *>(control<QQuickItem>()); + styleOption.window = effectiveWindow(window()); + styleOption.palette = QQuickItemPrivate::get(m_control)->palette()->toQPalette(); + styleOption.rect = QRect(QPoint(0, 0), imageSize()); + + styleOption.state = QStyle::State_None; + styleOption.state |= controlSize(styleOption.control); + + // Note: not all controls inherit from QQuickControl (e.g QQuickTextField) + if (const auto quickControl = dynamic_cast<QQuickControl *>(m_control.data())) + styleOption.direction = quickControl->isMirrored() ? Qt::RightToLeft : Qt::LeftToRight; + + if (styleOption.window) { + if (styleOption.window->isActive()) + styleOption.state |= QStyle::State_Active; + if (m_control->isEnabled()) + styleOption.state |= QStyle::State_Enabled; + if (m_control->hasActiveFocus()) + styleOption.state |= QStyle::State_HasFocus; + if (m_control->isUnderMouse()) + styleOption.state |= QStyle::State_MouseOver; + // Should this depend on the focusReason (e.g. only TabFocus) ? + styleOption.state |= QStyle::State_KeyboardFocusChange; + } + + if (m_overrideState != None) { + // In Button.qml we fade between two versions of + // the handle, depending on if it's hovered or not + if (m_overrideState & AlwaysHovered) + styleOption.state |= QStyle::State_MouseOver; + else if (m_overrideState & NeverHovered) + styleOption.state &= ~QStyle::State_MouseOver; + } + + qqc2Info() << styleOption; +} + +void QQuickStyleItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + QQuickItem::geometryChange(newGeometry, oldGeometry); + + // Ensure that we only schedule a new geometry update + // and polish if this geometry change was caused by + // something else than us already updating geometry. + if (!m_polishing) + markGeometryDirty(); +} + +void QQuickStyleItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) +{ + QQuickItem::itemChange(change, data); + + switch (change) { + case QQuickItem::ItemVisibleHasChanged: + if (data.boolValue) + markImageDirty(); + break; + case QQuickItem::ItemSceneChange: { + markImageDirty(); + QQuickWindow *win = data.window; + if (m_connectedWindow) + disconnect(m_connectedWindow, &QQuickWindow::activeChanged, this, &QQuickStyleItem::markImageDirty); + if (win) + connect(win, &QQuickWindow::activeChanged, this, &QQuickStyleItem::markImageDirty); + m_connectedWindow = win; + break;} + default: + break; + } +} + +bool QQuickStyleItem::event(QEvent *event) +{ + if (event->type() == QEvent::ApplicationPaletteChange) { + markImageDirty(); + if (auto *style = QQuickStyleItem::style()) + style->polish(); + } + + return QQuickItem::event(event); +} + +void QQuickStyleItem::updateGeometry() +{ + qqc2InfoHeading("GEOMETRY"); + m_dirty.setFlag(DirtyFlag::Geometry, false); + + const QQuickStyleMargins oldContentPadding = contentPadding(); + const QQuickStyleMargins oldLayoutMargins = layoutMargins(); + const QSize oldMinimumSize = minimumSize(); + + m_styleItemGeometry = calculateGeometry(); + +#ifdef QT_DEBUG + if (m_styleItemGeometry.minimumSize.isEmpty()) + qmlWarning(this) << "(StyleItem) minimumSize is empty!"; +#endif + + if (m_styleItemGeometry.implicitSize.isEmpty()) { + // If the item has no contents (or its size is + // empty), we just use the minimum size as implicit size. + m_styleItemGeometry.implicitSize = m_styleItemGeometry.minimumSize; + qqc2Info() << "implicitSize is empty, using minimumSize instead"; + } + +#ifdef QT_DEBUG + if (m_styleItemGeometry.implicitSize.width() < m_styleItemGeometry.minimumSize.width()) + qmlWarning(this) << "(StyleItem) implicit width is smaller than minimum width!"; + if (m_styleItemGeometry.implicitSize.height() < m_styleItemGeometry.minimumSize.height()) + qmlWarning(this) << "(StyleItem) implicit height is smaller than minimum height!"; +#endif + + if (contentPadding() != oldContentPadding) + emit contentPaddingChanged(); + if (layoutMargins() != oldLayoutMargins) + emit layoutMarginsChanged(); + if (minimumSize() != oldMinimumSize) + emit minimumSizeChanged(); + + setImplicitSize(m_styleItemGeometry.implicitSize.width(), m_styleItemGeometry.implicitSize.height()); + + qqc2Info() << m_styleItemGeometry + << "bounding rect:" << boundingRect() + << "layout margins:" << layoutMargins() + << "content padding:" << contentPadding() + << "input content size:" << m_contentSize; +} + +void QQuickStyleItem::paintControlToImage() +{ + qqc2InfoHeading("PAINT"); + const QSize imgSize = imageSize(); + if (imgSize.isEmpty()) + return; + + m_dirty.setFlag(DirtyFlag::Image, false); + + // The size of m_paintedImage should normally be imgSize * dpr. The problem is + // that the dpr can be e.g 1.25, which means that the size can end up having a + // fraction. But an image cannot have a size with a fraction, so it would need + // to be rounded. But on the flip side, rounding the size means that the size + // of the scene graph node (which is, when the texture is not scaled, + // m_paintedImage.size() / dpr), will end up with a fraction instead. And this + // causes rendering artifacts in the scene graph when the texture is mapped + // to physical screen coordinates. So for that reason we calculate an image size + // that might be slightly larger than imgSize, so that imgSize * dpr lands on a + // whole number. The result is that neither the image size, nor the scene graph + // node, ends up with a size that has a fraction. + const qreal dpr = window()->effectiveDevicePixelRatio(); + const int alignedW = int(dprAlignedSize(imgSize.width()) * dpr); + const int alignedH = int(dprAlignedSize(imgSize.height()) * dpr); + const QSize alignedSize = QSize(alignedW, alignedH); + + if (m_paintedImage.size() != alignedSize) { + m_paintedImage = QImage(alignedSize, QImage::Format_ARGB32_Premultiplied); + m_paintedImage.setDevicePixelRatio(dpr); + qqc2Info() << "created image with dpr aligned size:" << alignedSize; + } + + m_paintedImage.fill(Qt::transparent); + + QPainter painter(&m_paintedImage); + paintEvent(&painter); + +#ifdef QT_DEBUG + if (m_debugFlags != NoDebug) { + painter.setPen(QColor(255, 0, 0, 255)); + if (m_debugFlags.testFlag(ImageRect)) + painter.drawRect(QRect(QPoint(0, 0), alignedSize / dpr)); + if (m_debugFlags.testFlag(LayoutRect)) { + const auto m = layoutMargins(); + QRect rect = QRect(QPoint(0, 0), imgSize); + rect.adjust(m.left(), m.top(), -m.right(), -m.bottom()); + painter.drawRect(rect); + } + if (m_debugFlags.testFlag(ContentRect)) { + const auto p = contentPadding(); + QRect rect = QRect(QPoint(0, 0), imgSize); + rect.adjust(p.left(), p.top(), -p.right(), -p.bottom()); + painter.drawRect(rect); + } + if (m_debugFlags.testFlag(InputContentSize)) { + const int offset = 2; + const QPoint p = m_styleItemGeometry.contentRect.topLeft(); + painter.drawLine(p.x() - offset, p.y() - offset, p.x() + m_contentSize.width(), p.y() - offset); + painter.drawLine(p.x() - offset, p.y() - offset, p.x() - offset, p.y() + m_contentSize.height()); + } + if (m_debugFlags.testFlag(NinePatchMargins)) { + const QMargins m = m_styleItemGeometry.ninePatchMargins; + if (m.right() != -1) { + painter.drawLine(m.left(), 0, m.left(), imgSize.height()); + painter.drawLine(imgSize.width() - m.right(), 0, imgSize.width() - m.right(), imgSize.height()); + } + if (m.bottom() != -1) { + painter.drawLine(0, m.top(), imgSize.width(), m.top()); + painter.drawLine(0, imgSize.height() - m.bottom(), imgSize.width(), imgSize.height() - m.bottom()); + } + } + if (m_debugFlags.testFlag(SaveImage)) { + static int nr = -1; + ++nr; + static QString filename = QStringLiteral("styleitem_saveimage_"); + const QString path = QDir::current().absoluteFilePath(filename); + const QString name = path + QString::number(nr) + QStringLiteral(".png"); + m_paintedImage.save(name); + qDebug() << "image saved to:" << name; + } + } +#endif + + update(); +} + +void QQuickStyleItem::updatePolish() +{ + QScopedValueRollback<bool> guard(m_polishing, true); + + const bool dirtyGeometry = m_dirty & DirtyFlag::Geometry; + const bool dirtyImage = isVisible() && ((m_dirty & DirtyFlag::Image) || (!m_useNinePatchImage && dirtyGeometry)); + + if (dirtyGeometry) + updateGeometry(); + if (dirtyImage) + paintControlToImage(); +} + +#ifdef QT_DEBUG +void QQuickStyleItem::addDebugInfo() +{ + // Example debug strings: + // "QQC2_NATIVESTYLE_DEBUG="myButton info contentRect" + // "QQC2_NATIVESTYLE_DEBUG="ComboBox ninepatchmargins" + // "QQC2_NATIVESTYLE_DEBUG="All layoutrect" + + static const auto debugString = qEnvironmentVariable("QQC2_NATIVESTYLE_DEBUG"); + static const auto matchAll = debugString.startsWith(QLatin1String("All ")); + static const auto prefix = QStringLiteral("QQuickStyleItem"); + if (debugString.isEmpty()) + return; + + const auto objectName = m_control->objectName(); + const auto typeName = QString::fromUtf8(metaObject()->className()).remove(prefix); + const bool matchName = !objectName.isEmpty() && debugString.startsWith(objectName); + const bool matchType = debugString.startsWith(typeName); + + if (!(matchAll || matchName || matchType)) + return; + +#define QQC2_DEBUG_FLAG(FLAG) \ + if (debugString.contains(QLatin1String(#FLAG), Qt::CaseInsensitive)) m_debugFlags |= FLAG + + QQC2_DEBUG_FLAG(Info); + QQC2_DEBUG_FLAG(ImageRect); + QQC2_DEBUG_FLAG(ContentRect); + QQC2_DEBUG_FLAG(LayoutRect); + QQC2_DEBUG_FLAG(InputContentSize); + QQC2_DEBUG_FLAG(DontUseNinePatchImage); + QQC2_DEBUG_FLAG(NinePatchMargins); + QQC2_DEBUG_FLAG(Unscaled); + QQC2_DEBUG_FLAG(Debug); + QQC2_DEBUG_FLAG(SaveImage); + + if (m_debugFlags & (DontUseNinePatchImage + | InputContentSize + | ContentRect + | LayoutRect + | NinePatchMargins)) { + // Some rects will not fit inside the drawn image unless + // we switch off (nine patch) image scaling. + m_debugFlags |= DontUseNinePatchImage; + m_useNinePatchImage = false; + } + + if (m_debugFlags != NoDebug) + qDebug() << "debug options set for" << typeName << "(" << objectName << "):" << m_debugFlags; + else + qDebug() << "available debug options:" << DebugFlags(0xFFFF); +} +#endif + +void QQuickStyleItem::componentComplete() +{ + Q_ASSERT_X(m_control, Q_FUNC_INFO, "You need to assign a value to property 'control'"); +#ifdef QT_DEBUG + addDebugInfo(); +#endif + QQuickItem::componentComplete(); + updateGeometry(); + connectToControl(); + polish(); +} + +qreal QQuickStyleItem::contentWidth() +{ + return m_contentSize.width(); +} + +void QQuickStyleItem::setContentWidth(qreal contentWidth) +{ + if (qFuzzyCompare(m_contentSize.width(), contentWidth)) + return; + + m_contentSize.setWidth(contentWidth); + markGeometryDirty(); +} + +qreal QQuickStyleItem::contentHeight() +{ + return m_contentSize.height(); +} + +void QQuickStyleItem::setContentHeight(qreal contentHeight) +{ + if (qFuzzyCompare(m_contentSize.height(), contentHeight)) + return; + + m_contentSize.setHeight(contentHeight); + markGeometryDirty(); +} + +QQuickStyleMargins QQuickStyleItem::contentPadding() const +{ + const QRect outerRect(QPoint(0, 0), m_styleItemGeometry.implicitSize); + return QQuickStyleMargins(outerRect, m_styleItemGeometry.contentRect); +} + +QQuickStyleMargins QQuickStyleItem::layoutMargins() const +{ + // ### TODO: layoutRect is currently not being used for anything. But + // eventually this information will be needed by layouts to align the controls + // correctly. This because the images drawn by QStyle are usually a bit bigger + // than the control(frame) itself, to e.g make room for shadow effects + // or focus rects/glow. And this will differ from control to control. The + // layoutRect will then inform where the frame of the control is. + QQuickStyleMargins margins; + if (m_styleItemGeometry.layoutRect.isValid()) { + const QRect outerRect(QPoint(0, 0), m_styleItemGeometry.implicitSize); + margins = QQuickStyleMargins(outerRect, m_styleItemGeometry.layoutRect); + } + return margins; +} + +QSize QQuickStyleItem::minimumSize() const +{ + // The style item should not be scaled below this size. + // Otherwise the image will be truncated. + return m_styleItemGeometry.minimumSize; +} + +QSize QQuickStyleItem::imageSize() const +{ + // Returns the size of the QImage (unscaled) that + // is used to draw the control from QStyle. + return m_useNinePatchImage ? m_styleItemGeometry.minimumSize : size().toSize(); +} + +qreal QQuickStyleItem::focusFrameRadius() const +{ + return m_styleItemGeometry.focusFrameRadius; +} + +QFont QQuickStyleItem::styleFont(QQuickItem *control) const +{ + Q_ASSERT(control); + // Note: This function should be treated as if it was static + // (meaning, don't assume anything in this object to be initialized). + // Resolving the font/font size should be done early on from QML, before we get + // around to calculate geometry and paint. Otherwise we typically need to do it + // all over again when/if the font changes. In practice this means that other + // items in QML that uses a style font, and at the same time, affects our input + // contentSize, cannot wait for this item to be fully constructed before it + // gets the font. So we need to resolve it here and now, even if this + // object might be in a half initialized state (hence also the control + // argument, instead of relying on m_control to be set). + return QGuiApplication::font(); +} + +QT_END_NAMESPACE + +#include "moc_qquickstyleitem.cpp" |