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, p->parentItem);
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
292 QPointF requestedPos(p->x, p->y);
293 // Shift the window position a bit back, so that the top-left of the
294 // background frame ends up at the requested position.
295 QPointF windowPos = requestedPos - p->windowInsetsTopLeft();
296
297 if (!p->popupWindow || !p->parentItem) {
298 // If we don't have a popupWindow, set a temporary effective pos. Otherwise
299 // wait for a callback to QQuickPopupWindow::handlePopupPositionChangeFromWindowSystem()
300 // from setting p->popupWindow->setPosition() below.
301 p->setEffectivePosFromWindowPos(windowPos);
302 return;
303 }
304
305 // Wayland does server side repositioning.
306 // All other platforms handle the repositioning on the client side.
307 if (QGuiApplication::platformName().startsWith(QLatin1String("wayland")))
308 return;
309
310 QQuickPopupItem *popupItem = static_cast<QQuickPopupItem *>(m_popup->popupItem());
311 const QQuickItem *centerInParent = p->anchors ? p->getAnchors()->centerIn() : nullptr;
312 const QQuickOverlay *centerInOverlay = qobject_cast<const QQuickOverlay *>(centerInParent);
313 bool skipFittingStep = false;
314
315 if (centerInOverlay) {
316 windowPos = QPoint(qRound((centerInOverlay->width() - p->popupItem->width()) / 2.0),
317 qRound((centerInOverlay->height() - p->popupItem->height()) / 2.0));
318 skipFittingStep = true;
319 } else if (centerInParent == p->parentItem) {
320 windowPos = QPoint(qRound((p->parentItem->width() - p->popupItem->width()) / 2.0),
321 qRound((p->parentItem->height() - p->popupItem->height()) / 2.0));
322 skipFittingStep = true;
323 } else if (centerInParent)
324 qmlWarning(popup()) << "Popup can only be centered within its immediate parent or Overlay.overlay";
325
326 const QPointF globalCoords = centerInOverlay ? centerInOverlay->mapToGlobal(windowPos.x(), windowPos.y())
327 : p->parentItem->mapToGlobal(windowPos.x(), windowPos.y());
328 QRectF rect = { globalCoords.x(), globalCoords.y(), popupItem->width(), popupItem->height() };
329
330 if (!skipFittingStep) {
331 const QScreen *screenAtPopupPosition = QGuiApplication::screenAt(globalCoords.toPoint());
332 const QScreen *screen = screenAtPopupPosition ? screenAtPopupPosition : QGuiApplication::primaryScreen();
333 const QRectF bounds = screen->availableGeometry().toRectF();
334
335 // When flipping menus, we need to take both the overlap and padding into account.
336 const qreal overlap = popup()->property("overlap").toReal();
337 qreal padding = 0;
338 qreal scale = 1.0;
339 if (const QQuickPopup *parentPopup = qobject_cast<QQuickPopup *>(popup()->parent())) {
340 padding = parentPopup->leftPadding();
341 scale = parentPopup->scale();
342 }
343
344 if (p->allowHorizontalFlip && (rect.left() < bounds.left() || rect.right() > bounds.right()))
345 rect.moveLeft(rect.left() - requestedPos.x() - rect.width() + overlap * scale - padding);
346
347 if (p->allowVerticalFlip && (rect.top() < bounds.top() || rect.bottom() > bounds.bottom()))
348 rect.moveTop(rect.top() - requestedPos.y() - rect.height() + overlap * scale);
349
350 if (rect.left() < bounds.left() || rect.right() > bounds.right()) {
351 if (p->allowHorizontalMove) {
352 if (rect.left() < bounds.left() && bounds.left() + rect.width() <= bounds.right())
353 rect.moveLeft(bounds.left());
354 else if (rect.right() > bounds.right() && bounds.right() - rect.width() >= bounds.left())
355 rect.moveRight(bounds.right());
356 }
357 }
358 if (rect.top() < bounds.top() || rect.bottom() > bounds.bottom()) {
359 if (p->allowVerticalMove) {
360 if (rect.top() < bounds.top() && bounds.top() + rect.height() <= bounds.bottom())
361 rect.moveTop(bounds.top());
362 else if (rect.bottom() > bounds.bottom() && bounds.bottom() - rect.height() >= bounds.top())
363 rect.moveBottom(bounds.bottom());
364 }
365 }
366 }
367
368 p->popupWindow->setPosition(rect.x(), rect.y());
369 p->popupItem->setPosition(p->windowInsetsTopLeft());
370}
371
372void QQuickPopupPositioner::itemGeometryChanged(QQuickItem *, QQuickGeometryChange, const QRectF &)
373{
374 auto *popupPrivate = QQuickPopupPrivate::get(m_popup);
375 if (m_parentItem && m_popup->popupItem()->isVisible() && popupPrivate->resolvedPopupType() == QQuickPopup::PopupType::Item)
376 popupPrivate->reposition();
377}
378
379void QQuickPopupPositioner::itemParentChanged(QQuickItem *, QQuickItem *parent)
380{
381 addAncestorListeners(parent);
382}
383
384void QQuickPopupPositioner::itemChildRemoved(QQuickItem *item, QQuickItem *child)
385{
386 if (child == m_parentItem || child->isAncestorOf(m_parentItem))
387 removeAncestorListeners(item);
388}
389
390void QQuickPopupPositioner::removeAncestorListeners(QQuickItem *item)
391{
392 if (item == m_parentItem)
393 return;
394
395 QQuickItem *p = item;
396 while (p) {
397 QQuickItemPrivate::get(p)->removeItemChangeListener(this, AncestorChangeTypes);
398 p = p->parentItem();
399 }
400}
401
402void QQuickPopupPositioner::addAncestorListeners(QQuickItem *item)
403{
404 if (item == m_parentItem)
405 return;
406
407 QQuickItem *p = item;
408 while (p) {
409 QQuickItemPrivate::get(p)->updateOrAddItemChangeListener(this, AncestorChangeTypes);
410 p = p->parentItem();
411 }
412}
413
414QT_END_NAMESPACE
Combined button and popup list for selecting options.
static const QQuickItemPrivate::ChangeTypes ItemChangeTypes