1// Copyright (C) 2020 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquickstyleitem.h"
5
6#include <QtCore/qscopedvaluerollback.h>
7#include <QtCore/qdir.h>
8
9#include <QtQuick/qsgninepatchnode.h>
10#include <QtQuick/private/qquickwindow_p.h>
11#include <QtQuick/qquickwindow.h>
12#include <QtQuick/qquickrendercontrol.h>
13
14#include <QtQuickTemplates2/private/qquickcontrol_p.h>
15#include <QtQuickTemplates2/private/qquickbutton_p.h>
16
17#include <QtQml/qqml.h>
18
19#include "qquickstyleitembutton.h"
20#include "qquickstylehelper_p.h"
21
22QT_BEGIN_NAMESPACE
23
24QDebug operator<<(QDebug debug, const QQuickStyleMargins &padding)
25{
26 QDebugStateSaver saver(debug);
27 debug.nospace();
28 debug << "StyleMargins(";
29 debug << padding.left() << ", ";
30 debug << padding.top() << ", ";
31 debug << padding.right() << ", ";
32 debug << padding.bottom();
33 debug << ')';
34 return debug;
35}
36
37QDebug operator<<(QDebug debug, const StyleItemGeometry &cg)
38{
39 QDebugStateSaver saver(debug);
40 debug.nospace();
41 debug << "StyleItemGeometry(";
42 debug << "implicitSize:" << cg.implicitSize << ", ";
43 debug << "contentRect:" << cg.contentRect << ", ";
44 debug << "layoutRect:" << cg.layoutRect << ", ";
45 debug << "minimumSize:" << cg.minimumSize << ", ";
46 debug << "9patchMargins:" << cg.ninePatchMargins;
47 debug << ')';
48 return debug;
49}
50
51int QQuickStyleItem::dprAlignedSize(const int size) const
52{
53 // Return the first value equal to or bigger than size
54 // that is a whole number when multiplied with the dpr.
55 static int multiplier = [&]() {
56 const qreal dpr = window()->effectiveDevicePixelRatio();
57 for (int m = 1; m <= 10; ++m) {
58 const qreal v = m * dpr;
59 if (v == int(v))
60 return m;
61 }
62
63 qWarning() << "The current dpr (" << dpr << ") is not supported"
64 << "by the style and might result in drawing artifacts";
65 return 1;
66 }();
67
68 return int(qCeil(v: qreal(size) / qreal(multiplier)) * multiplier);
69}
70
71QQuickStyleItem::QQuickStyleItem(QQuickItem *parent)
72 : QQuickItem(parent)
73{
74 setFlag(flag: QQuickItem::ItemHasContents);
75}
76
77QQuickStyleItem::~QQuickStyleItem()
78{
79}
80
81void QQuickStyleItem::connectToControl() const
82{
83 connect(sender: m_control, signal: &QQuickStyleItem::enabledChanged, context: this, slot: &QQuickStyleItem::markImageDirty);
84 connect(sender: m_control, signal: &QQuickItem::activeFocusChanged, context: this, slot: &QQuickStyleItem::markImageDirty);
85
86 if (QQuickWindow *win = window()) {
87 connect(sender: win, signal: &QQuickWindow::activeChanged, context: this, slot: &QQuickStyleItem::markImageDirty);
88 m_connectedWindow = win;
89 }
90}
91
92void QQuickStyleItem::markImageDirty()
93{
94 m_dirty.setFlag(flag: DirtyFlag::Image);
95 if (isComponentComplete())
96 polish();
97}
98
99void QQuickStyleItem::markGeometryDirty()
100{
101 m_dirty.setFlag(flag: DirtyFlag::Geometry);
102 if (isComponentComplete())
103 polish();
104}
105
106QSGNode *QQuickStyleItem::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
107{
108 QSGNinePatchNode *node = static_cast<QSGNinePatchNode *>(oldNode);
109 if (!node)
110 node = window()->createNinePatchNode();
111
112 if (m_paintedImage.isNull()) {
113 // If we cannot create a texture, the node should not exist either
114 // because its material requires a texture.
115 delete node;
116 return nullptr;
117 }
118
119 const auto texture = window()->createTextureFromImage(image: m_paintedImage, options: QQuickWindow::TextureCanUseAtlas);
120
121 QRectF bounds = boundingRect();
122 const qreal dpr = window()->effectiveDevicePixelRatio();
123 const QSizeF unscaledImageSize = QSizeF(m_paintedImage.size()) / dpr;
124
125 // We can scale the image up with a nine patch node, but should
126 // avoid to scale it down. Otherwise the nine patch image will look
127 // wrapped (or look truncated, in case of no padding). So if the
128 // item is smaller that the image, don't scale.
129 if (bounds.width() < unscaledImageSize.width())
130 bounds.setWidth(unscaledImageSize.width());
131 if (bounds.height() < unscaledImageSize.height())
132 bounds.setHeight(unscaledImageSize.height());
133
134#ifdef QT_DEBUG
135 if (m_debugFlags.testFlag(flag: Unscaled)) {
136 bounds.setSize(unscaledImageSize);
137 qqc2Info() << "Setting qsg node size to the unscaled size of m_paintedImage:" << bounds;
138 }
139#endif
140
141 if (m_useNinePatchImage) {
142 QMargins padding = m_styleItemGeometry.ninePatchMargins;
143 if (padding.right() == -1) {
144 // Special case: a padding of -1 means that
145 // the image shouldn't scale in the given direction.
146 padding.setLeft(0);
147 padding.setRight(0);
148 }
149 if (padding.bottom() == -1) {
150 padding.setTop(0);
151 padding.setBottom(0);
152 }
153 node->setPadding(left: padding.left(), top: padding.top(), right: padding.right(), bottom: padding.bottom());
154 }
155
156 node->setBounds(bounds);
157 node->setTexture(texture);
158 node->setDevicePixelRatio(dpr);
159 node->update();
160
161 return node;
162}
163
164QStyle::State QQuickStyleItem::controlSize(QQuickItem *item)
165{
166 // TODO: add proper API for small and mini
167 if (item->metaObject()->indexOfProperty(name: "qqc2_style_small") != -1)
168 return QStyle::State_Small;
169 if (item->metaObject()->indexOfProperty(name: "qqc2_style_mini") != -1)
170 return QStyle::State_Mini;
171 return QStyle::State_None;
172}
173
174static QWindow *effectiveWindow(QQuickWindow *window)
175{
176 QWindow *renderWindow = QQuickRenderControl::renderWindowFor(win: window);
177 return renderWindow ? renderWindow : window;
178}
179
180void QQuickStyleItem::initStyleOptionBase(QStyleOption &styleOption) const
181{
182 Q_ASSERT(m_control);
183
184 styleOption.control = const_cast<QQuickItem *>(control<QQuickItem>());
185 styleOption.window = effectiveWindow(window: window());
186 styleOption.palette = QQuickItemPrivate::get(item: m_control)->palette()->toQPalette();
187 styleOption.rect = QRect(QPoint(0, 0), imageSize());
188
189 styleOption.state = QStyle::State_None;
190 styleOption.state |= controlSize(item: styleOption.control);
191
192 // Note: not all controls inherit from QQuickControl (e.g QQuickTextField)
193 if (const auto quickControl = dynamic_cast<QQuickControl *>(m_control.data()))
194 styleOption.direction = quickControl->isMirrored() ? Qt::RightToLeft : Qt::LeftToRight;
195
196 if (styleOption.window) {
197 if (styleOption.window->isActive())
198 styleOption.state |= QStyle::State_Active;
199 if (m_control->isEnabled())
200 styleOption.state |= QStyle::State_Enabled;
201 if (m_control->hasActiveFocus())
202 styleOption.state |= QStyle::State_HasFocus;
203 if (m_control->isUnderMouse())
204 styleOption.state |= QStyle::State_MouseOver;
205 // Should this depend on the focusReason (e.g. only TabFocus) ?
206 styleOption.state |= QStyle::State_KeyboardFocusChange;
207 }
208
209 if (m_overrideState != None) {
210 // In Button.qml we fade between two versions of
211 // the handle, depending on if it's hovered or not
212 if (m_overrideState & AlwaysHovered)
213 styleOption.state |= QStyle::State_MouseOver;
214 else if (m_overrideState & NeverHovered)
215 styleOption.state &= ~QStyle::State_MouseOver;
216 }
217
218 qqc2Info() << styleOption;
219}
220
221void QQuickStyleItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
222{
223 QQuickItem::geometryChange(newGeometry, oldGeometry);
224
225 // Ensure that we only schedule a new geometry update
226 // and polish if this geometry change was caused by
227 // something else than us already updating geometry.
228 if (!m_polishing)
229 markGeometryDirty();
230}
231
232void QQuickStyleItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data)
233{
234 QQuickItem::itemChange(change, data);
235
236 switch (change) {
237 case QQuickItem::ItemVisibleHasChanged:
238 if (data.boolValue)
239 markImageDirty();
240 break;
241 case QQuickItem::ItemSceneChange: {
242 markImageDirty();
243 QQuickWindow *win = data.window;
244 if (m_connectedWindow)
245 disconnect(sender: m_connectedWindow, signal: &QQuickWindow::activeChanged, receiver: this, slot: &QQuickStyleItem::markImageDirty);
246 if (win)
247 connect(sender: win, signal: &QQuickWindow::activeChanged, context: this, slot: &QQuickStyleItem::markImageDirty);
248 m_connectedWindow = win;
249 break;}
250 default:
251 break;
252 }
253}
254
255bool QQuickStyleItem::event(QEvent *event)
256{
257 if (event->type() == QEvent::ApplicationPaletteChange) {
258 markImageDirty();
259 if (auto *style = QQuickStyleItem::style())
260 style->polish();
261 }
262
263 return QQuickItem::event(event);
264}
265
266void QQuickStyleItem::updateGeometry()
267{
268 qqc2InfoHeading("GEOMETRY");
269 m_dirty.setFlag(flag: DirtyFlag::Geometry, on: false);
270
271 const QQuickStyleMargins oldContentPadding = contentPadding();
272 const QQuickStyleMargins oldLayoutMargins = layoutMargins();
273 const QSize oldMinimumSize = minimumSize();
274
275 m_styleItemGeometry = calculateGeometry();
276
277#ifdef QT_DEBUG
278 if (m_styleItemGeometry.minimumSize.isEmpty())
279 qmlWarning(me: this) << "(StyleItem) minimumSize is empty!";
280#endif
281
282 if (m_styleItemGeometry.implicitSize.isEmpty()) {
283 // If the item has no contents (or its size is
284 // empty), we just use the minimum size as implicit size.
285 m_styleItemGeometry.implicitSize = m_styleItemGeometry.minimumSize;
286 qqc2Info() << "implicitSize is empty, using minimumSize instead";
287 }
288
289#ifdef QT_DEBUG
290 if (m_styleItemGeometry.implicitSize.width() < m_styleItemGeometry.minimumSize.width())
291 qmlWarning(me: this) << "(StyleItem) implicit width is smaller than minimum width!";
292 if (m_styleItemGeometry.implicitSize.height() < m_styleItemGeometry.minimumSize.height())
293 qmlWarning(me: this) << "(StyleItem) implicit height is smaller than minimum height!";
294#endif
295
296 if (contentPadding() != oldContentPadding)
297 emit contentPaddingChanged();
298 if (layoutMargins() != oldLayoutMargins)
299 emit layoutMarginsChanged();
300 if (minimumSize() != oldMinimumSize)
301 emit minimumSizeChanged();
302
303 setImplicitSize(m_styleItemGeometry.implicitSize.width(), m_styleItemGeometry.implicitSize.height());
304
305 qqc2Info() << m_styleItemGeometry
306 << "bounding rect:" << boundingRect()
307 << "layout margins:" << layoutMargins()
308 << "content padding:" << contentPadding()
309 << "input content size:" << m_contentSize;
310}
311
312void QQuickStyleItem::paintControlToImage()
313{
314 qqc2InfoHeading("PAINT");
315 const QSize imgSize = imageSize();
316 if (imgSize.isEmpty())
317 return;
318
319 m_dirty.setFlag(flag: DirtyFlag::Image, on: false);
320
321 // The size of m_paintedImage should normally be imgSize * dpr. The problem is
322 // that the dpr can be e.g 1.25, which means that the size can end up having a
323 // fraction. But an image cannot have a size with a fraction, so it would need
324 // to be rounded. But on the flip side, rounding the size means that the size
325 // of the scene graph node (which is, when the texture is not scaled,
326 // m_paintedImage.size() / dpr), will end up with a fraction instead. And this
327 // causes rendering artifacts in the scene graph when the texture is mapped
328 // to physical screen coordinates. So for that reason we calculate an image size
329 // that might be slightly larger than imgSize, so that imgSize * dpr lands on a
330 // whole number. The result is that neither the image size, nor the scene graph
331 // node, ends up with a size that has a fraction.
332 const qreal dpr = window()->effectiveDevicePixelRatio();
333 const int alignedW = int(dprAlignedSize(size: imgSize.width()) * dpr);
334 const int alignedH = int(dprAlignedSize(size: imgSize.height()) * dpr);
335 const QSize alignedSize = QSize(alignedW, alignedH);
336
337 if (m_paintedImage.size() != alignedSize) {
338 m_paintedImage = QImage(alignedSize, QImage::Format_ARGB32_Premultiplied);
339 m_paintedImage.setDevicePixelRatio(dpr);
340 qqc2Info() << "created image with dpr aligned size:" << alignedSize;
341 }
342
343 m_paintedImage.fill(color: Qt::transparent);
344
345 QPainter painter(&m_paintedImage);
346 paintEvent(painter: &painter);
347
348#ifdef QT_DEBUG
349 if (m_debugFlags != NoDebug) {
350 painter.setPen(QColor(255, 0, 0, 255));
351 if (m_debugFlags.testFlag(flag: ImageRect))
352 painter.drawRect(r: QRect(QPoint(0, 0), alignedSize / dpr));
353 if (m_debugFlags.testFlag(flag: LayoutRect)) {
354 const auto m = layoutMargins();
355 QRect rect = QRect(QPoint(0, 0), imgSize);
356 rect.adjust(dx1: m.left(), dy1: m.top(), dx2: -m.right(), dy2: -m.bottom());
357 painter.drawRect(r: rect);
358 }
359 if (m_debugFlags.testFlag(flag: ContentRect)) {
360 const auto p = contentPadding();
361 QRect rect = QRect(QPoint(0, 0), imgSize);
362 rect.adjust(dx1: p.left(), dy1: p.top(), dx2: -p.right(), dy2: -p.bottom());
363 painter.drawRect(r: rect);
364 }
365 if (m_debugFlags.testFlag(flag: InputContentSize)) {
366 const int offset = 2;
367 const QPoint p = m_styleItemGeometry.contentRect.topLeft();
368 painter.drawLine(x1: p.x() - offset, y1: p.y() - offset, x2: p.x() + m_contentSize.width(), y2: p.y() - offset);
369 painter.drawLine(x1: p.x() - offset, y1: p.y() - offset, x2: p.x() - offset, y2: p.y() + m_contentSize.height());
370 }
371 if (m_debugFlags.testFlag(flag: NinePatchMargins)) {
372 const QMargins m = m_styleItemGeometry.ninePatchMargins;
373 if (m.right() != -1) {
374 painter.drawLine(x1: m.left(), y1: 0, x2: m.left(), y2: imgSize.height());
375 painter.drawLine(x1: imgSize.width() - m.right(), y1: 0, x2: imgSize.width() - m.right(), y2: imgSize.height());
376 }
377 if (m.bottom() != -1) {
378 painter.drawLine(x1: 0, y1: m.top(), x2: imgSize.width(), y2: m.top());
379 painter.drawLine(x1: 0, y1: imgSize.height() - m.bottom(), x2: imgSize.width(), y2: imgSize.height() - m.bottom());
380 }
381 }
382 if (m_debugFlags.testFlag(flag: SaveImage)) {
383 static int nr = -1;
384 ++nr;
385 static QString filename = QStringLiteral("styleitem_saveimage_");
386 const QString path = QDir::current().absoluteFilePath(fileName: filename);
387 const QString name = path + QString::number(nr) + QStringLiteral(".png");
388 m_paintedImage.save(fileName: name);
389 qDebug() << "image saved to:" << name;
390 }
391 }
392#endif
393
394 update();
395}
396
397void QQuickStyleItem::updatePolish()
398{
399 QScopedValueRollback<bool> guard(m_polishing, true);
400
401 const bool dirtyGeometry = m_dirty & DirtyFlag::Geometry;
402 const bool dirtyImage = isVisible() && ((m_dirty & DirtyFlag::Image) || (!m_useNinePatchImage && dirtyGeometry));
403
404 if (dirtyGeometry)
405 updateGeometry();
406 if (dirtyImage)
407 paintControlToImage();
408}
409
410#ifdef QT_DEBUG
411void QQuickStyleItem::addDebugInfo()
412{
413 // Example debug strings:
414 // "QQC2_NATIVESTYLE_DEBUG="myButton info contentRect"
415 // "QQC2_NATIVESTYLE_DEBUG="ComboBox ninepatchmargins"
416 // "QQC2_NATIVESTYLE_DEBUG="All layoutrect"
417
418 static const auto debugString = qEnvironmentVariable(varName: "QQC2_NATIVESTYLE_DEBUG");
419 static const auto matchAll = debugString.startsWith(s: QLatin1String("All "));
420 static const auto prefix = QStringLiteral("QQuickStyleItem");
421 if (debugString.isEmpty())
422 return;
423
424 const auto objectName = m_control->objectName();
425 const auto typeName = QString::fromUtf8(utf8: metaObject()->className()).remove(s: prefix);
426 const bool matchName = !objectName.isEmpty() && debugString.startsWith(s: objectName);
427 const bool matchType = debugString.startsWith(s: typeName);
428
429 if (!(matchAll || matchName || matchType))
430 return;
431
432#define QQC2_DEBUG_FLAG(FLAG) \
433 if (debugString.contains(QLatin1String(#FLAG), Qt::CaseInsensitive)) m_debugFlags |= FLAG
434
435 QQC2_DEBUG_FLAG(Info);
436 QQC2_DEBUG_FLAG(ImageRect);
437 QQC2_DEBUG_FLAG(ContentRect);
438 QQC2_DEBUG_FLAG(LayoutRect);
439 QQC2_DEBUG_FLAG(InputContentSize);
440 QQC2_DEBUG_FLAG(DontUseNinePatchImage);
441 QQC2_DEBUG_FLAG(NinePatchMargins);
442 QQC2_DEBUG_FLAG(Unscaled);
443 QQC2_DEBUG_FLAG(Debug);
444 QQC2_DEBUG_FLAG(SaveImage);
445
446 if (m_debugFlags & (DontUseNinePatchImage
447 | InputContentSize
448 | ContentRect
449 | LayoutRect
450 | NinePatchMargins)) {
451 // Some rects will not fit inside the drawn image unless
452 // we switch off (nine patch) image scaling.
453 m_debugFlags |= DontUseNinePatchImage;
454 m_useNinePatchImage = false;
455 }
456
457 if (m_debugFlags != NoDebug)
458 qDebug() << "debug options set for" << typeName << "(" << objectName << "):" << m_debugFlags;
459 else
460 qDebug() << "available debug options:" << DebugFlags(0xFFFF);
461}
462#endif
463
464void QQuickStyleItem::componentComplete()
465{
466 Q_ASSERT_X(m_control, Q_FUNC_INFO, "You need to assign a value to property 'control'");
467#ifdef QT_DEBUG
468 addDebugInfo();
469#endif
470 QQuickItem::componentComplete();
471 updateGeometry();
472 connectToControl();
473 polish();
474}
475
476qreal QQuickStyleItem::contentWidth()
477{
478 return m_contentSize.width();
479}
480
481void QQuickStyleItem::setContentWidth(qreal contentWidth)
482{
483 if (qFuzzyCompare(p1: m_contentSize.width(), p2: contentWidth))
484 return;
485
486 m_contentSize.setWidth(contentWidth);
487 markGeometryDirty();
488}
489
490qreal QQuickStyleItem::contentHeight()
491{
492 return m_contentSize.height();
493}
494
495void QQuickStyleItem::setContentHeight(qreal contentHeight)
496{
497 if (qFuzzyCompare(p1: m_contentSize.height(), p2: contentHeight))
498 return;
499
500 m_contentSize.setHeight(contentHeight);
501 markGeometryDirty();
502}
503
504QQuickStyleMargins QQuickStyleItem::contentPadding() const
505{
506 const QRect outerRect(QPoint(0, 0), m_styleItemGeometry.implicitSize);
507 return QQuickStyleMargins(outerRect, m_styleItemGeometry.contentRect);
508}
509
510QQuickStyleMargins QQuickStyleItem::layoutMargins() const
511{
512 // ### TODO: layoutRect is currently not being used for anything. But
513 // eventually this information will be needed by layouts to align the controls
514 // correctly. This because the images drawn by QStyle are usually a bit bigger
515 // than the control(frame) itself, to e.g make room for shadow effects
516 // or focus rects/glow. And this will differ from control to control. The
517 // layoutRect will then inform where the frame of the control is.
518 QQuickStyleMargins margins;
519 if (m_styleItemGeometry.layoutRect.isValid()) {
520 const QRect outerRect(QPoint(0, 0), m_styleItemGeometry.implicitSize);
521 margins = QQuickStyleMargins(outerRect, m_styleItemGeometry.layoutRect);
522 }
523 return margins;
524}
525
526QSize QQuickStyleItem::minimumSize() const
527{
528 // The style item should not be scaled below this size.
529 // Otherwise the image will be truncated.
530 return m_styleItemGeometry.minimumSize;
531}
532
533QSize QQuickStyleItem::imageSize() const
534{
535 // Returns the size of the QImage (unscaled) that
536 // is used to draw the control from QStyle.
537 return m_useNinePatchImage ? m_styleItemGeometry.minimumSize : size().toSize();
538}
539
540qreal QQuickStyleItem::focusFrameRadius() const
541{
542 return m_styleItemGeometry.focusFrameRadius;
543}
544
545QFont QQuickStyleItem::styleFont(QQuickItem *control) const
546{
547 Q_ASSERT(control);
548 // Note: This function should be treated as if it was static
549 // (meaning, don't assume anything in this object to be initialized).
550 // Resolving the font/font size should be done early on from QML, before we get
551 // around to calculate geometry and paint. Otherwise we typically need to do it
552 // all over again when/if the font changes. In practice this means that other
553 // items in QML that uses a style font, and at the same time, affects our input
554 // contentSize, cannot wait for this item to be fully constructed before it
555 // gets the font. So we need to resolve it here and now, even if this
556 // object might be in a half initialized state (hence also the control
557 // argument, instead of relying on m_control to be set).
558 return QGuiApplication::font();
559}
560
561QT_END_NAMESPACE
562
563#include "moc_qquickstyleitem.cpp"
564

source code of qtdeclarative/src/quicknativestyle/items/qquickstyleitem.cpp