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
qquickpopuppositioner.cpp
Go to the documentation of this file.
1// Copyright (C) 2017 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
11
12#include <QtCore/qloggingcategory.h>
13#include <QtQml/qqmlinfo.h>
14#include <QtQuick/private/qquickitem_p.h>
15
17
18Q_STATIC_LOGGING_CATEGORY(lcPopupPositioner, "qt.quick.controls.popuppositioner")
19
20static const QQuickItemPrivate::ChangeTypes AncestorChangeTypes = QQuickItemPrivate::Geometry
21 | QQuickItemPrivate::Parent
22 | QQuickItemPrivate::Children;
23
24static const QQuickItemPrivate::ChangeTypes ItemChangeTypes = QQuickItemPrivate::Geometry
25 | QQuickItemPrivate::Parent;
26
27QQuickPopupPositioner::QQuickPopupPositioner(QQuickPopup *popup)
28 : m_popup(popup)
29{
30}
31
32QQuickPopupPositioner::~QQuickPopupPositioner()
33{
34 if (m_parentItem) {
35 QQuickItemPrivate::get(m_parentItem)->removeItemChangeListener(this, ItemChangeTypes);
36 removeAncestorListeners(m_parentItem->parentItem());
37 }
38}
39
40QQuickPopup *QQuickPopupPositioner::popup() const
41{
42 return m_popup;
43}
44
45QQuickItem *QQuickPopupPositioner::parentItem() const
46{
47 return m_parentItem;
48}
49
50void QQuickPopupPositioner::setParentItem(QQuickItem *parent)
51{
52 if (m_parentItem == parent)
53 return;
54
55 if (m_parentItem) {
56 QQuickItemPrivate::get(m_parentItem)->removeItemChangeListener(this, ItemChangeTypes);
57 removeAncestorListeners(m_parentItem->parentItem());
58 }
59
60 m_parentItem = parent;
61
62 if (!parent)
63 return;
64
65 QQuickItemPrivate::get(parent)->addItemChangeListener(this, ItemChangeTypes);
66 addAncestorListeners(parent->parentItem());
67 // Store the scale property so the end result of any transition that could effect the scale
68 // does not influence the top left of the final popup, so it doesn't appear to flip from one
69 // position to another as a result
70 m_popupScale = m_popup->popupItem()->scale();
71 if (m_popup->popupItem()->isVisible())
72 QQuickPopupPrivate::get(m_popup)->reposition();
73}
74
75void QQuickPopupPositioner::reposition()
76{
77 auto p = QQuickPopupPrivate::get(popup());
78 QQuickPopupItem *popupItem = static_cast<QQuickPopupItem *>(m_popup->popupItem());
79
80 if (p->usePopupWindow()) {
81 repositionPopupWindow();
82 return;
83 }
84
85 if (!popupItem->isVisible())
86 return;
87
88 if (m_positioning) {
89 popupItem->polish();
90 return;
91 }
92
93 qCDebug(lcPopupPositioner) << "reposition called for" << m_popup;
94
95 const qreal w = popupItem->width() * m_popupScale;
96 const qreal h = popupItem->height() * m_popupScale;
97 const qreal iw = popupItem->implicitWidth() * m_popupScale;
98 const qreal ih = popupItem->implicitHeight() * m_popupScale;
99
100 bool widthAdjusted = false;
101 bool heightAdjusted = false;
102
103 const QQuickItem *centerInParent = p->anchors ? p->getAnchors()->centerIn() : nullptr;
104 const QQuickOverlay *centerInOverlay = qobject_cast<const QQuickOverlay*>(centerInParent);
105 QRectF rect(!centerInParent ? p->allowHorizontalMove ? p->x : popupItem->x() : 0,
106 !centerInParent ? p->allowVerticalMove ? p->y : popupItem->y() : 0,
107 !p->hasWidth && iw > 0 ? iw : w, !p->hasHeight && ih > 0 ? ih : h);
108 bool relaxEdgeConstraint = p->relaxEdgeConstraint;
109 if (m_parentItem) {
110 // m_parentItem is the parent that the popup should open in,
111 // and popupItem()->parentItem() is the overlay, so the mapToItem() calls below
112 // effectively map the rect to scene coordinates.
113
114 // Animations can cause reposition() to get called when m_parentItem no longer has a window.
115 if (!m_parentItem->window())
116 return;
117
118 if (centerInParent) {
119 if (centerInParent != parentItem() && !centerInOverlay) {
120 qmlWarning(m_popup) << "Popup can only be centered within its immediate parent or Overlay.overlay";
121 return;
122 }
123
124 if (centerInOverlay) {
125 rect.moveCenter(QPointF(qRound(centerInOverlay->width() / 2.0), qRound(centerInOverlay->height() / 2.0)));
126 // Popup cannot be moved outside window bounds when its centered with overlay
127 relaxEdgeConstraint = false;
128 } else {
129 const QPointF parentItemCenter = QPointF(qRound(m_parentItem->width() / 2), qRound(m_parentItem->height() / 2));
130 rect.moveCenter(m_parentItem->mapToItem(popupItem->parentItem(), parentItemCenter));
131 }
132 } else {
133 rect.moveTopLeft(m_parentItem->mapToItem(popupItem->parentItem(), rect.topLeft()));
134 }
135
136 // The overlay is assumed to fully cover the window's contents, although the overlay's geometry
137 // might not always equal the window's geometry (for example, if the window's contents are rotated).
138 QQuickOverlay *overlay = QQuickOverlay::overlay(p->window);
139 if (overlay) {
140 qreal boundsWidth = overlay->width();
141 qreal boundsHeight = overlay->height();
142
143 // QTBUG-126843: On some platforms, the overlay's geometry is not yet available at the instant
144 // when Component.completed() is emitted. Fall back to the window's geometry for this edge case.
145 if (Q_UNLIKELY(boundsWidth <= 0)) {
146 boundsWidth = p->window->width();
147 boundsHeight = p->window->height();
148 }
149
150 const QMarginsF margins = p->getMargins();
151 QRectF bounds(qMax<qreal>(0.0, margins.left()),
152 qMax<qreal>(0.0, margins.top()),
153 boundsWidth - qMax<qreal>(0.0, margins.left()) - qMax<qreal>(0.0, margins.right()),
154 boundsHeight - qMax<qreal>(0.0, margins.top()) - qMax<qreal>(0.0, margins.bottom()));
155
156 // if the popup doesn't fit horizontally inside the window, try flipping it around (left <-> right)
157 if (p->allowHorizontalFlip && (rect.left() < bounds.left() || rect.right() > bounds.right())) {
158 const QPointF newTopLeft(m_parentItem->width() - p->x - rect.width(), p->y);
159 const QRectF flipped(m_parentItem->mapToItem(popupItem->parentItem(), newTopLeft),
160 rect.size());
161 if (flipped.intersected(bounds).width() > rect.intersected(bounds).width())
162 rect.moveLeft(flipped.left());
163 }
164
165 // if the popup doesn't fit vertically inside the window, try flipping it around (above <-> below)
166 if (p->allowVerticalFlip && (rect.top() < bounds.top() || rect.bottom() > bounds.bottom())) {
167 const QPointF newTopLeft(p->x, m_parentItem->height() - p->y - rect.height());
168 const QRectF flipped(m_parentItem->mapToItem(popupItem->parentItem(), newTopLeft),
169 rect.size());
170 if (flipped.intersected(bounds).height() > rect.intersected(bounds).height())
171 rect.moveTop(flipped.top());
172 }
173
174 // push inside the margins if specified
175 if (p->allowVerticalMove) {
176 if (margins.top() >= 0 && rect.top() < bounds.top())
177 rect.moveTop(margins.top());
178 if (margins.bottom() >= 0 && rect.bottom() > bounds.bottom())
179 rect.moveBottom(bounds.bottom());
180 }
181 if (p->allowHorizontalMove) {
182 if (margins.left() >= 0 && rect.left() < bounds.left())
183 rect.moveLeft(margins.left());
184 if (margins.right() >= 0 && rect.right() > bounds.right())
185 rect.moveRight(bounds.right());
186 }
187
188 if (iw > 0 && (rect.left() < bounds.left() || rect.right() > bounds.right())) {
189 // neither the flipped or pushed geometry fits inside the window, choose
190 // whichever side (left vs. right) fits larger part of the popup
191 if (p->allowHorizontalMove && p->allowHorizontalFlip) {
192 if (rect.left() < bounds.left() && bounds.left() + rect.width() <= bounds.right())
193 rect.moveLeft(bounds.left());
194 else if (rect.right() > bounds.right() && bounds.right() - rect.width() >= bounds.left())
195 rect.moveRight(bounds.right());
196 }
197
198 // as a last resort, adjust the width to fit the window
199 // Negative margins don't require resize as popup not pushed within
200 // the boundary. But otherwise, retain existing behavior of resizing
201 // for items, such as menus, which enables flip.
202 if (p->allowHorizontalResize) {
203 if ((margins.left() >= 0 || !relaxEdgeConstraint)
204 && (rect.left() < bounds.left())) {
205 rect.setLeft(bounds.left());
206 widthAdjusted = true;
207 }
208 if ((margins.right() >= 0 || !relaxEdgeConstraint)
209 && (rect.right() > bounds.right())) {
210 rect.setRight(bounds.right());
211 widthAdjusted = true;
212 }
213 }
214 } else if (iw > 0 && rect.left() >= bounds.left() && rect.right() <= bounds.right()
215 && iw != w) {
216 // restore original width
217 rect.setWidth(iw);
218 widthAdjusted = true;
219 }
220
221 if (ih > 0 && (rect.top() < bounds.top() || rect.bottom() > bounds.bottom())) {
222 // neither the flipped or pushed geometry fits inside the window, choose
223 // whichever side (above vs. below) fits larger part of the popup
224 if (p->allowVerticalMove && p->allowVerticalFlip) {
225 if (rect.top() < bounds.top() && bounds.top() + rect.height() <= bounds.bottom())
226 rect.moveTop(bounds.top());
227 else if (rect.bottom() > bounds.bottom() && bounds.bottom() - rect.height() >= bounds.top())
228 rect.moveBottom(bounds.bottom());
229 }
230
231 // as a last resort, adjust the height to fit the window
232 // Negative margins don't require resize as popup not pushed within
233 // the boundary. But otherwise, retain existing behavior of resizing
234 // for items, such as menus, which enables flip.
235 if (p->allowVerticalResize) {
236 if ((margins.top() >= 0 || !relaxEdgeConstraint)
237 && (rect.top() < bounds.top())) {
238 rect.setTop(bounds.top());
239 heightAdjusted = true;
240 }
241 if ((margins.bottom() >= 0 || !relaxEdgeConstraint)
242 && (rect.bottom() > bounds.bottom())) {
243 rect.setBottom(bounds.bottom());
244 heightAdjusted = true;
245 }
246 }
247 } else if (ih > 0 && rect.top() >= bounds.top() && rect.bottom() <= bounds.bottom()
248 && ih != h) {
249 // restore original height
250 rect.setHeight(ih);
251 heightAdjusted = true;
252 }
253 }
254 }
255
256 m_positioning = true;
257
258 const QPointF windowPos = rect.topLeft();
259 popupItem->setPosition(windowPos);
260
261 // If the popup was assigned a parent, rect will be in scene coordinates,
262 // so we need to map its top left back to item coordinates.
263 // However, if centering within the overlay, the coordinates will be relative
264 // to the window, so we don't need to do anything.
265 // The same applies to popups that are in their own dedicated window.
266 if (m_parentItem && !centerInOverlay)
267 p->setEffectivePosFromWindowPos(m_parentItem->mapFromScene(windowPos));
268 else
269 p->setEffectivePosFromWindowPos(windowPos);
270
271 if (!p->hasWidth && widthAdjusted && rect.width() > 0) {
272 popupItem->setWidth(rect.width() / m_popupScale);
273 // The popup doesn't have an explicit width, so we should respect that by not
274 // making our call above an explicit assignment. If we don't, the popup won't
275 // resize after being repositioned in some cases.
276 QQuickItemPrivate::get(popupItem)->widthValidFlag = false;
277 }
278 if (!p->hasHeight && heightAdjusted && rect.height() > 0) {
279 popupItem->setHeight(rect.height() / m_popupScale);
280 QQuickItemPrivate::get(popupItem)->heightValidFlag = false;
281 }
282 m_positioning = false;
283
284 qCDebug(lcPopupPositioner) << "- new popupItem geometry:"
285 << popupItem->x() << popupItem->y() << popupItem->width() << popupItem->height();
286}
287
288void QQuickPopupPositioner::repositionPopupWindow()
289{
290 auto *p = QQuickPopupPrivate::get(popup());
291 QQuickPopupItem *popupItem = static_cast<QQuickPopupItem *>(m_popup->popupItem());
292
293 QPointF requestedPos(p->x, p->y);
294 // Shift the window position a bit back, so that the top-left of the
295 // background frame ends up at the requested position.
296 QPointF windowPos = requestedPos - p->windowInsetsTopLeft();
297
298 if (!p->popupWindow || !p->parentItem) {
299 // If we don't have a popupWindow, set a temporary effective pos. Otherwise
300 // wait for a callback to QQuickPopupWindow::handlePopupPositionChangeFromWindowSystem()
301 // from setting p->popupWindow->setPosition() below.
302 p->setEffectivePosFromWindowPos(windowPos);
303 return;
304 }
305
306 const QQuickItem *centerInParent = p->anchors ? p->getAnchors()->centerIn() : nullptr;
307 const QQuickOverlay *centerInOverlay = qobject_cast<const QQuickOverlay *>(centerInParent);
308 bool skipFittingStep = false;
309
310 if (centerInOverlay) {
311 windowPos = QPoint(qRound((centerInOverlay->width() - p->popupItem->width()) / 2.0),
312 qRound((centerInOverlay->height() - p->popupItem->height()) / 2.0));
313 skipFittingStep = true;
314 } else if (centerInParent == p->parentItem) {
315 windowPos = QPoint(qRound((p->parentItem->width() - p->popupItem->width()) / 2.0),
316 qRound((p->parentItem->height() - p->popupItem->height()) / 2.0));
317 skipFittingStep = true;
318 } else if (centerInParent)
319 qmlWarning(popup()) << "Popup can only be centered within its immediate parent or Overlay.overlay";
320
321 const QPointF globalCoords = centerInOverlay ? centerInOverlay->mapToGlobal(windowPos.x(), windowPos.y())
322 : p->parentItem->mapToGlobal(windowPos.x(), windowPos.y());
323 QRectF rect = { globalCoords.x(), globalCoords.y(), popupItem->width(), popupItem->height() };
324
325 // QTBUG-99618: On wayland, we can't use QWindow::mapToGlobal(), and should use a xdg_positioner instead.
326 static bool isWayland = QGuiApplication::platformName().startsWith(QLatin1String("wayland"));
327 if (!skipFittingStep && !isWayland) {
328 const QScreen *screenAtPopupPosition = QGuiApplication::screenAt(globalCoords.toPoint());
329 const QScreen *screen = screenAtPopupPosition ? screenAtPopupPosition : QGuiApplication::primaryScreen();
330 const QRectF bounds = screen->availableGeometry().toRectF();
331
332 // When flipping menus, we need to take both the overlap and padding into account.
333 const qreal overlap = popup()->property("overlap").toReal();
334 qreal padding = 0;
335 qreal scale = 1.0;
336 if (const QQuickPopup *parentPopup = qobject_cast<QQuickPopup *>(popup()->parent())) {
337 padding = parentPopup->leftPadding();
338 scale = parentPopup->scale();
339 }
340
341 if (p->allowHorizontalFlip && (rect.left() < bounds.left() || rect.right() > bounds.right()))
342 rect.moveLeft(rect.left() - requestedPos.x() - rect.width() + overlap * scale - padding);
343
344 if (p->allowVerticalFlip && (rect.top() < bounds.top() || rect.bottom() > bounds.bottom()))
345 rect.moveTop(rect.top() - requestedPos.y() - rect.height() + overlap * scale);
346
347 if (rect.left() < bounds.left() || rect.right() > bounds.right()) {
348 if (p->allowHorizontalMove) {
349 if (rect.left() < bounds.left() && bounds.left() + rect.width() <= bounds.right())
350 rect.moveLeft(bounds.left());
351 else if (rect.right() > bounds.right() && bounds.right() - rect.width() >= bounds.left())
352 rect.moveRight(bounds.right());
353 }
354 }
355 if (rect.top() < bounds.top() || rect.bottom() > bounds.bottom()) {
356 if (p->allowVerticalMove) {
357 if (rect.top() < bounds.top() && bounds.top() + rect.height() <= bounds.bottom())
358 rect.moveTop(bounds.top());
359 else if (rect.bottom() > bounds.bottom() && bounds.bottom() - rect.height() >= bounds.top())
360 rect.moveBottom(bounds.bottom());
361 }
362 }
363 }
364
365 p->popupWindow->setPosition(rect.x(), rect.y());
366 p->popupItem->setPosition(p->windowInsetsTopLeft());
367}
368
369void QQuickPopupPositioner::itemGeometryChanged(QQuickItem *, QQuickGeometryChange, const QRectF &)
370{
371 auto *popupPrivate = QQuickPopupPrivate::get(m_popup);
372 if (m_parentItem && m_popup->popupItem()->isVisible() && popupPrivate->resolvedPopupType() == QQuickPopup::PopupType::Item)
373 popupPrivate->reposition();
374}
375
376void QQuickPopupPositioner::itemParentChanged(QQuickItem *, QQuickItem *parent)
377{
378 addAncestorListeners(parent);
379}
380
381void QQuickPopupPositioner::itemChildRemoved(QQuickItem *item, QQuickItem *child)
382{
383 if (child == m_parentItem || child->isAncestorOf(m_parentItem))
384 removeAncestorListeners(item);
385}
386
387void QQuickPopupPositioner::removeAncestorListeners(QQuickItem *item)
388{
389 if (item == m_parentItem)
390 return;
391
392 QQuickItem *p = item;
393 while (p) {
394 QQuickItemPrivate::get(p)->removeItemChangeListener(this, AncestorChangeTypes);
395 p = p->parentItem();
396 }
397}
398
399void QQuickPopupPositioner::addAncestorListeners(QQuickItem *item)
400{
401 if (item == m_parentItem)
402 return;
403
404 QQuickItem *p = item;
405 while (p) {
406 QQuickItemPrivate::get(p)->updateOrAddItemChangeListener(this, AncestorChangeTypes);
407 p = p->parentItem();
408 }
409}
410
411QT_END_NAMESPACE
static const QQuickItemPrivate::ChangeTypes ItemChangeTypes