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
qquicksafearea.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 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
5#include <QtQuick/private/qquicksafearea_p.h>
6
7#include <QtQuick/private/qquickanchors_p_p.h>
8#include <QtQuick/private/qquickitem_p.h>
9#include <QtQuick/private/qquickflickable_p.h>
10#include <QtQuick/qquickwindow.h>
11#include <QtQuick/qquickitem.h>
12
14
15Q_STATIC_LOGGING_CATEGORY(lcSafeArea, "qt.quick.safearea", QtWarningMsg)
16
17/*!
18 \qmltype SafeArea
19 \nativetype QQuickSafeArea
20 \inqmlmodule QtQuick
21 \ingroup qtquick-visual
22 \since 6.9
23 \brief Provides access to the safe area properties of the item or window.
24
25 The SafeArea attached type provides information about the areas of
26 an Item or Window where content may risk being overlapped by other
27 UI elements, such as system title bars or status bars.
28
29 This information can be used to lay out children of an item within
30 the safe area of the item, while still allowing a background color
31 or effect to span the entire item.
32
33 \table
34 \row
35 \li \snippet qml/safearea/basic.qml 0
36 \li \inlineimage safearea-ios.webp
37 {iPhone showing content respecting safe area margins}
38 \endtable
39
40 The SafeArea margins are relative to the item they attach to. If an
41 ancestor item has laid out its children within the safe area margins,
42 any descendant item with its own SafeArea attached will report zero
43 margins, unless \l{Additional margins}{additional margins} have been
44 added.
45
46 \note An item should not be positioned based on \e{its own} safe area,
47 as that would result in a binding loop.
48
49 \section2 Additional margins
50
51 Sometimes an item's layout involves child items that overlap each other,
52 for example in a window with a semi transparent header, where the rest
53 of the window content flows underneath the header.
54
55 In this scenario, the item may reflect the header's position and size
56 to the child items via the additionalMargins property.
57
58 The additional margins will be added to any margins that the
59 item already picks up from its parent hierarchy (including system
60 margins, such as title bars or status bars), and child items will
61 reflect the combined margins accordingly.
62
63 \table
64 \row
65 \li \snippet qml/safearea/additional.qml 0
66 \li \br \inlineimage safearea-ios-header.webp
67 {iPhone with header adding additional safe area margin}
68 \endtable
69
70 In the example above, the header item is positioned at the top of
71 the window, which may potentially overlap with existing safe area
72 margins coming from the window. To account for this we only add
73 additional margins for the part of the header that extends beyond
74 the window's safe area margins.
75
76 \note In this example the header item does not overlap the child item,
77 as the goal is to show how the items are positioned and resized in
78 response to safe area margin changes.
79
80 \section2 Controls
81
82 Applying safe area margins to a Control is straightforward,
83 as Control already offers properties to add padding to the
84 control's content item.
85
86 \snippet qml/safearea/controls.qml 0
87 */
88
89QQuickSafeArea *QQuickSafeArea::qmlAttachedProperties(QObject *attachee)
90{
91 auto *item = qobject_cast<QQuickItem*>(attachee);
92 if (!item) {
93 if (auto *window = qobject_cast<QQuickWindow*>(attachee))
94 item = window->contentItem();
95 }
96 if (!item) {
97 if (auto *safeAreaAttachable = qobject_cast<QQuickSafeAreaAttachable*>(attachee))
98 item = safeAreaAttachable->safeAreaAttachmentItem();
99 }
100 if (!item) {
101 qmlWarning(attachee) << "SafeArea can not be attached to this type";
102 return nullptr;
103 }
104
105 // We may already have created a safe area for Window, and are now
106 // requesting one for Window.contentItem (or the other way around).
107 // As both map to the same safe area item, we need to check first
108 // if we already have created one for this item.
109 if (auto *safeArea = item->findChild<QQuickSafeArea*>(Qt::FindDirectChildrenOnly))
110 return safeArea;
111
112 return new QQuickSafeArea(item);
113}
114
115QQuickSafeArea::QQuickSafeArea(QQuickItem *item)
116 : QObject(item)
117{
118 qCInfo(lcSafeArea) << "Creating" << this;
119
120 connect(item, &QQuickItem::windowChanged,
121 this, &QQuickSafeArea::windowChanged);
122
123 item->setFlag(QQuickItem::ItemObservesViewport);
124 QQuickItemPrivate::get(item)->addItemChangeListener(
125 this, QQuickItemPrivate::Matrix);
126
127 updateSafeArea();
128}
129
130QQuickSafeArea::~QQuickSafeArea()
131{
132 qCInfo(lcSafeArea) << "Destroying" << this;
133
134 const auto listenedItems = m_listenedItems;
135 for (const auto &item : listenedItems) {
136 if (!item)
137 continue;
138 auto *itemPrivate = QQuickItemPrivate::get(item);
139 itemPrivate->removeItemChangeListener(this,
140 QQuickItemPrivate::Matrix);
141 itemPrivate->removeItemChangeListener(this,
142 QQuickItemPrivate::Geometry);
143 }
144}
145
146/*!
147 \qmlpropertygroup QtQuick::SafeArea::margins
148 \qmlproperty real QtQuick::SafeArea::margins.top
149 \qmlproperty real QtQuick::SafeArea::margins.left
150 \qmlproperty real QtQuick::SafeArea::margins.right
151 \qmlproperty real QtQuick::SafeArea::margins.bottom
152 \readonly
153
154 This property holds the safe area margins, relative
155 to the attached item.
156
157 \sa additionalMargins
158 */
159QMarginsF QQuickSafeArea::margins() const
160{
161 return m_safeAreaMargins;
162}
163
164/*!
165 \qmlpropertygroup QtQuick::SafeArea::additionalMargins
166 \qmlproperty real QtQuick::SafeArea::additionalMargins.top
167 \qmlproperty real QtQuick::SafeArea::additionalMargins.left
168 \qmlproperty real QtQuick::SafeArea::additionalMargins.right
169 \qmlproperty real QtQuick::SafeArea::additionalMargins.bottom
170
171 This property holds the additional safe area margins for the item.
172
173 The additional safe area margins can not be negative, and will be
174 automatically clamped to 0.
175
176 The resulting safe area margins of the item are the sum of the inherited
177 margins (for example from title bars or status bar) and the additional
178 margins applied to the item.
179
180 \sa margins
181 */
182
183void QQuickSafeArea::setAdditionalMargins(const QMarginsF &additionalMargins)
184{
185 // Additional margins should never be negative
186 auto newMargins = additionalMargins | QMarginsF();
187
188 if (newMargins == m_additionalMargins)
189 return;
190
191 m_additionalMargins = newMargins;
192
193 emit additionalMarginsChanged();
194
195 auto *attachedItem = qobject_cast<QQuickItem*>(parent());
196 updateSafeAreasRecursively(attachedItem);
197}
198
199QMarginsF QQuickSafeArea::additionalMargins() const
200{
201 return m_additionalMargins;
202}
203
204/*
205 Maps the safe area \a margins from \a fromItem to \a toItem
206*/
207static QMarginsF toLocalMargins(const QMarginsF &margins, QQuickItem *fromItem, QQuickItem *toItem)
208{
209 if (margins.isNull())
210 return margins;
211
212 const auto localMarginRect = fromItem->mapRectToItem(toItem,
213 QRectF(margins.left(), margins.top(),
214 fromItem->width() - margins.left() - margins.right(),
215 fromItem->height() - margins.top() - margins.bottom()));
216
217 // Only return a mapped margin if there was an original margin
218 return QMarginsF(
219 margins.left() > 0 ? localMarginRect.left() : 0,
220 margins.top() > 0 ? localMarginRect.top() : 0,
221 margins.right() > 0 ? toItem->width() - localMarginRect.right() : 0,
222 margins.bottom() > 0 ? toItem->height() - localMarginRect.bottom() : 0
223 ) | QMarginsF();
224}
225
226void QQuickSafeArea::updateSafeArea()
227{
228 qCDebug(lcSafeArea) << "✨ Updating" << this;
229
230 auto *attachedItem = qobject_cast<QQuickItem*>(parent());
231 if (!QQuickItemPrivate::get(attachedItem)->componentComplete) {
232 qCDebug(lcSafeArea) << attachedItem << "is not complete. Deferring";
233 return;
234 }
235
236 QMarginsF inheritedMargins;
237 auto *parentItem = attachedItem->parentItem();
238 while (parentItem) {
239 if (qobject_cast<QQuickFlickable*>(parentItem)) {
240 // Stop propagation of safe areas when we hit a Flickable,
241 // as items within the content item that account for safe
242 // area margins will continuously update when the content
243 // item is moved, which is not necessarily what the user
244 // expects.
245 qCDebug(lcSafeArea) << "Stopping safe area margin propagation on" << parentItem;
246 break;
247 }
248
249
250 // We attach the safe area to the relevant item for an attachee
251 // such as QQuickWindow or QQuickPopup, so we can't go via
252 // qmlAttachedPropertiesObject to find the safe area for an
253 // item, as the attached object cache is based on the original
254 // attachee.
255 if (auto *safeArea = parentItem->findChild<QQuickSafeArea*>(Qt::FindDirectChildrenOnly)) {
256 inheritedMargins = safeArea->margins();
257 break;
258 }
259
260 parentItem = parentItem->parentItem();
261 }
262
263 const auto *window = attachedItem->window();
264 if (!parentItem && window) {
265 // We didn't find a parent item with a safe area,
266 // so inherit the margins from the window.
267 parentItem = window->contentItem();
268 inheritedMargins = window->safeAreaMargins();
269 }
270
271 auto inheritedMarginsMapped = toLocalMargins(inheritedMargins, parentItem, attachedItem);
272
273 // Make sure margins are never negative
274 const QMarginsF newMargins = QMarginsF() | (inheritedMarginsMapped + additionalMargins());
275
276 if (newMargins != m_safeAreaMargins) {
277 qCDebug(lcSafeArea) << "Margins changed from" << m_safeAreaMargins
278 << "to" << newMargins
279 << "based on inherited" << inheritedMargins
280 << "mapped to local" << inheritedMarginsMapped
281 << "and additional" << additionalMargins();
282
283 m_safeAreaMargins = newMargins;
284
285 if (emittingMarginsUpdate) {
286 // We are already in the process of emitting an update for this
287 // safe area, which resulted in the safe area margins changing.
288 // This can be a binding loop if the margins do not stabilize,
289 // which we'll detect when we return from the root emit below.
290 qCDebug(lcSafeArea) << "Already emitting update for" << this;
291 return;
292 }
293
294 QScopedValueRollback blocker(emittingMarginsUpdate, true);
295 emit marginsChanged();
296
297 if (m_safeAreaMargins != newMargins) {
298 qCDebug(lcSafeArea) << "⚠️ Possible binding loop for" << this
299 << newMargins << "changed to" << m_safeAreaMargins;
300
301 QScopedValueRollback blocker(detectedPossibleBindingLoop, true);
302
303 for (int i = 0; i < 5; ++i) {
304 auto marginsBeforeEmit = m_safeAreaMargins;
305 emit marginsChanged();
306 if (m_safeAreaMargins == marginsBeforeEmit) {
307 qCDebug(lcSafeArea) << "✅ Margins stabilized for" << this;
308 return;
309 }
310
311 qCDebug(lcSafeArea) << qPrintable(QStringLiteral("‼️").repeated(i + 1))
312 << marginsBeforeEmit << "changed to" << m_safeAreaMargins;
313 }
314
315 qmlWarning(attachedItem) << "Safe area binding loop detected";
316 }
317 }
318}
319
320void QQuickSafeArea::windowChanged()
321{
322 updateSafeArea();
323}
324
325void QQuickSafeArea::itemTransformChanged(QQuickItem *item, QQuickItem *transformedItem)
326{
327 Q_ASSERT(item == parent());
328
329 auto *transformedItemPrivate = QQuickItemPrivate::get(transformedItem);
330 qCDebug(lcSafeArea) << "📏 Transform changed for" << transformedItem
331 << "with dirty state" << transformedItemPrivate->dirtyToString();
332
333 if (qobject_cast<QQuickFlickable*>(transformedItem->parentItem())) {
334 qCDebug(lcSafeArea) << "Ignoring transform change for Flickable content item";
335 return;
336 }
337
338 // The order of transform and geometry change callbacks may not be in paint order,
339 // so to ensure we update the safe areas in paint order we find the item closest
340 // to the transformed item with a safe area, and let that safe area trigger the
341 // update recursively in paint order.
342 if (transformedItem != item) {
343 for (auto *parent = item->parentItem(); parent; parent = parent->parentItem()) {
344 if (parent->findChild<QQuickSafeArea*>(Qt::FindDirectChildrenOnly))
345 item = parent;
346
347 if (parent == transformedItem)
348 break;
349 }
350 }
351
352 if (item != parent()) {
353 qCDebug(lcSafeArea) << "Found" << item << "closer to transformed item than" << this;
354 return;
355 }
356
357 // The dirtying of position and size will be followed by a geometry change,
358 // which via anchors or event listeners may result in an ancestor invalidating
359 // its transform, which might invalidate the margins we're about to compute.
360 // Instead of processing the margin change now, possibly resulting in a flip-
361 // flop of the margins, we wait for the geometry notification, where the item
362 // hierarchy has already reacted to the geometry change of the transformed item.
363 // This accounts for anchors, and items that listen to geometry changes, but not
364 // property bindings, as those are emitted after notifying listeners (us) about
365 // the geometry change.
366 auto dirtyAttributes = transformedItemPrivate->dirtyAttributes;
367 if (dirtyAttributes & (QQuickItemPrivate::Position | QQuickItemPrivate::Size)) {
368 qCDebug(lcSafeArea) << "Deferring update of" << this << "until geometry change";
369 transformedItemPrivate->addItemChangeListener(
370 this, QQuickItemPrivate::Geometry);
371 return;
372 }
373
374 updateSafeAreasRecursively(item);
375}
376
377void QQuickSafeArea::itemGeometryChanged(QQuickItem *item, QQuickGeometryChange change, const QRectF &oldGeometry)
378{
379 Q_UNUSED(change);
380 Q_UNUSED(oldGeometry);
381
382 auto *itemPrivate = QQuickItemPrivate::get(item);
383 itemPrivate->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
384
385 qCDebug(lcSafeArea) << "📐 Geometry changed for" << item << "from" << oldGeometry
386 << "to" << QRectF(item->position(), item->size());
387
388 updateSafeAreasRecursively(item);
389}
390
391void QQuickSafeArea::updateSafeAreasRecursively(QQuickItem *item)
392{
393 Q_ASSERT(item);
394
395 if (auto *safeArea = item->findChild<QQuickSafeArea*>(Qt::FindDirectChildrenOnly))
396 safeArea->updateSafeArea();
397
398 auto *itemPrivate = QQuickItemPrivate::get(item);
399 const auto paintOrderChildItems = itemPrivate->paintOrderChildItems();
400 for (auto *child : paintOrderChildItems)
401 updateSafeAreasRecursively(child);
402}
403
404void QQuickSafeArea::addSourceItem(QQuickItem *item)
405{
406 m_listenedItems << item;
407}
408
409void QQuickSafeArea::removeSourceItem(QQuickItem *item)
410{
411 m_listenedItems.removeAll(item);
412}
413
414#ifndef QT_NO_DEBUG_STREAM
415QDebug operator<<(QDebug debug, const QQuickSafeArea *safeArea)
416{
417 QDebugStateSaver saver(debug);
418 debug.nospace();
419
420 if (!safeArea) {
421 debug << "QQuickSafeArea(nullptr)";
422 return debug;
423 }
424
425 debug << safeArea->metaObject()->className() << '(' << static_cast<const void *>(safeArea);
426
427 debug << ", attachedItem=" << safeArea->parent();
428 debug << ", safeAreaMargins=" << safeArea->m_safeAreaMargins;
429 debug << ", additionalMargins=" << safeArea->additionalMargins();
430
431 debug << ')';
432 return debug;
433}
434#endif // QT_NO_DEBUG_STREAM
435
436QQuickSafeAreaAttachable::~QQuickSafeAreaAttachable() = default;
437
438QT_END_NAMESPACE
439
440#include "moc_qquicksafearea_p.cpp"
QDebug operator<<(QDebug dbg, const NSObject *nsObject)
Definition qcore_mac.mm:209
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)
static QMarginsF toLocalMargins(const QMarginsF &margins, QQuickItem *fromItem, QQuickItem *toItem)