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