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

source code of qtdeclarative/src/quicktemplates/qquickpopuppositioner.cpp