Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qquickstyleitem.cpp
Go to the documentation of this file.
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// Qt-Security score:significant reason:default
4
6
7#include <private/qqc2qstylehelper_p.h>
8#include <private/qquickbutton_p.h>
9#include <private/qquickcontrol_p.h>
10#include <private/qquickstyleitembutton_p.h>
11#include <private/qquickwindow_p.h>
12
13#include <QtQuick/qquickrendercontrol.h>
14#include <QtQuick/qquickwindow.h>
15#include <QtQuick/qsgninepatchnode.h>
16
17#include <QtQml/qqml.h>
18
19#include <QtCore/qdir.h>
20#include <QtCore/qscopedvaluerollback.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(qreal(size) / qreal(multiplier)) * multiplier);
69}
70
71QQuickStyleItem::QQuickStyleItem(QQuickItem *parent)
72 : QQuickItem(parent)
73{
74 setFlag(QQuickItem::ItemHasContents);
75}
76
80
82{
83 connect(m_control, &QQuickStyleItem::enabledChanged, this, &QQuickStyleItem::markImageDirty);
84 connect(m_control, &QQuickItem::activeFocusChanged, this, &QQuickStyleItem::markImageDirty);
85
86 if (QQuickWindow *win = window()) {
87 connect(win, &QQuickWindow::activeChanged, this, &QQuickStyleItem::markImageDirty);
88 m_connectedWindow = win;
89 }
90}
91
93{
94 m_dirty.setFlag(DirtyFlag::Image);
95 if (isComponentComplete())
96 polish();
97}
98
100{
101 m_dirty.setFlag(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(m_paintedImage, 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(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(padding.left(), padding.top(), padding.right(), 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("qqc2_style_small") != -1)
168 return QStyle::State_Small;
169 if (item->metaObject()->indexOfProperty("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(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());
186 styleOption.palette = QQuickItemPrivate::get(m_control)->palette()->toQPalette();
187 styleOption.rect = QRect(QPoint(0, 0), imageSize());
188
189 styleOption.state = QStyle::State_None;
190 styleOption.state |= controlSize(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)
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)
240 break;
241 case QQuickItem::ItemSceneChange: {
243 QQuickWindow *win = data.window;
244 if (m_connectedWindow)
245 disconnect(m_connectedWindow, &QQuickWindow::activeChanged, this, &QQuickStyleItem::markImageDirty);
246 if (win)
247 connect(win, &QQuickWindow::activeChanged, this, &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) {
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(DirtyFlag::Geometry, 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(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(this) << "(StyleItem) implicit width is smaller than minimum width!";
292 if (m_styleItemGeometry.implicitSize.height() < m_styleItemGeometry.minimumSize.height())
293 qmlWarning(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(DirtyFlag::Image, 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(imgSize.width()) * dpr);
334 const int alignedH = int(dprAlignedSize(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(Qt::transparent);
344
345 QPainter painter(&m_paintedImage);
346 paintEvent(&painter);
347
348#ifdef QT_DEBUG
349 if (m_debugFlags != NoDebug) {
350 painter.setPen(QColor(255, 0, 0, 255));
351 if (m_debugFlags.testFlag(ImageRect))
352 painter.drawRect(QRect(QPoint(0, 0), alignedSize / dpr));
353 if (m_debugFlags.testFlag(LayoutRect)) {
354 const auto m = layoutMargins();
355 QRect rect = QRect(QPoint(0, 0), imgSize);
356 rect.adjust(m.left(), m.top(), -m.right(), -m.bottom());
357 painter.drawRect(rect);
358 }
359 if (m_debugFlags.testFlag(ContentRect)) {
360 const auto p = contentPadding();
361 QRect rect = QRect(QPoint(0, 0), imgSize);
362 rect.adjust(p.left(), p.top(), -p.right(), -p.bottom());
363 painter.drawRect(rect);
364 }
365 if (m_debugFlags.testFlag(InputContentSize)) {
366 const int offset = 2;
367 const QPoint p = m_styleItemGeometry.contentRect.topLeft();
368 painter.drawLine(p.x() - offset, p.y() - offset, p.x() + m_contentSize.width(), p.y() - offset);
369 painter.drawLine(p.x() - offset, p.y() - offset, p.x() - offset, p.y() + m_contentSize.height());
370 }
371 if (m_debugFlags.testFlag(NinePatchMargins)) {
372 const QMargins m = m_styleItemGeometry.ninePatchMargins;
373 if (m.right() != -1) {
374 painter.drawLine(m.left(), 0, m.left(), imgSize.height());
375 painter.drawLine(imgSize.width() - m.right(), 0, imgSize.width() - m.right(), imgSize.height());
376 }
377 if (m.bottom() != -1) {
378 painter.drawLine(0, m.top(), imgSize.width(), m.top());
379 painter.drawLine(0, imgSize.height() - m.bottom(), imgSize.width(), imgSize.height() - m.bottom());
380 }
381 }
382 if (m_debugFlags.testFlag(SaveImage)) {
383 static int nr = -1;
384 ++nr;
385 static QString filename = QStringLiteral("styleitem_saveimage_");
386 const QString path = QDir::current().absoluteFilePath(filename);
387 const QString name = path + QString::number(nr) + QStringLiteral(".png");
388 m_paintedImage.save(name);
389 qDebug() << "image saved to:" << name;
390 }
391 }
392#endif
393
394 update();
395}
396
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("QQC2_NATIVESTYLE_DEBUG");
419 static const auto matchAll = debugString.startsWith(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(metaObject()->className()).remove(prefix);
426 const bool matchName = !objectName.isEmpty() && debugString.startsWith(objectName);
427 const bool matchType = debugString.startsWith(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
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();
473 polish();
474}
475
477{
478 return m_contentSize.width();
479}
480
481void QQuickStyleItem::setContentWidth(qreal contentWidth)
482{
483 if (qFuzzyCompare(m_contentSize.width(), contentWidth))
484 return;
485
486 m_contentSize.setWidth(contentWidth);
488}
489
491{
492 return m_contentSize.height();
493}
494
495void QQuickStyleItem::setContentHeight(qreal contentHeight)
496{
497 if (qFuzzyCompare(m_contentSize.height(), contentHeight))
498 return;
499
500 m_contentSize.setHeight(contentHeight);
502}
503
505{
506 const QRect outerRect(QPoint(0, 0), m_styleItemGeometry.implicitSize);
507 return QQuickStyleMargins(outerRect, m_styleItemGeometry.contentRect);
508}
509
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
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
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
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_p.cpp"
virtual void connectToControl() const
void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override
qreal focusFrameRadius() const
QSize imageSize() const
void itemChange(ItemChange change, const ItemChangeData &data) override
Called when change occurs for this item.
qreal contentWidth()
void updatePolish() override
This function should perform any layout as required for this item.
void setContentHeight(qreal contentHeight)
QQuickStyleMargins contentPadding() const
~QQuickStyleItem() override
QQuickStyleMargins layoutMargins() const
QSize minimumSize() const
void setContentWidth(qreal contentWidth)
qreal contentHeight()
void initStyleOptionBase(QStyleOption &styleOption) const
QSGNode * updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override
Called on the render thread when it is time to sync the state of the item with the scene graph.
void componentComplete() override
\reimp Derived classes should call the base class method before adding their own actions to perform a...
bool event(QEvent *event) override
\reimp
QDebug operator<<(QDebug debug, const StyleItemGeometry &cg)
static QWindow * effectiveWindow(QQuickWindow *window)
#define qqc2Info()
#define qqc2InfoHeading(HEADING)