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