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 QPointF requestedPos(p->x, p->y);
81 // Shift the window position a bit back, so that the top-left of the
82 // background frame ends up at the requested position.
83 QPointF windowPos = requestedPos - p->windowInsetsTopLeft();
84
85 if (!p->popupWindow || !p->parentItem) {
86 // If we don't have a popupWindow, set a temporary effective pos. Otherwise
87 // wait for a callback to QQuickPopupWindow::handlePopupPositionChangeFromWindowSystem()
88 // from setting p->popupWindow->setPosition() below.
89 p->setEffectivePosFromWindowPos(windowPos);
90 return;
91 }
92
93 const QQuickItem *centerInParent = p->anchors ? p->getAnchors()->centerIn() : nullptr;
94 const QQuickOverlay *centerInOverlay = qobject_cast<const QQuickOverlay *>(object: centerInParent);
95
96 if (centerInOverlay) {
97 windowPos = QPoint(qRound(d: (centerInOverlay->width() - p->popupItem->width()) / 2.0),
98 qRound(d: (centerInOverlay->height() - p->popupItem->height()) / 2.0));
99 } else if (centerInParent == p->parentItem) {
100 windowPos = QPoint(qRound(d: (p->parentItem->width() - p->popupItem->width()) / 2.0),
101 qRound(d: (p->parentItem->height() - p->popupItem->height()) / 2.0));
102 } else if (centerInParent) {
103 qmlWarning(me: popup()) << "Popup can only be centered within its immediate parent or Overlay.overlay";
104 }
105
106 const QPointF globalCoords = centerInOverlay
107 ? centerInOverlay->mapToGlobal(x: windowPos.x(), y: windowPos.y())
108 : p->parentItem->mapToGlobal(x: windowPos.x(), y: windowPos.y());
109
110 p->popupWindow->setPosition(posx: globalCoords.x(), posy: globalCoords.y());
111 p->popupItem->setPosition(p->windowInsetsTopLeft());
112 return;
113 }
114
115 if (!popupItem->isVisible())
116 return;
117
118 if (m_positioning) {
119 popupItem->polish();
120 return;
121 }
122
123 qCDebug(lcPopupPositioner) << "reposition called for" << m_popup;
124
125 const qreal w = popupItem->width() * m_popupScale;
126 const qreal h = popupItem->height() * m_popupScale;
127 const qreal iw = popupItem->implicitWidth() * m_popupScale;
128 const qreal ih = popupItem->implicitHeight() * m_popupScale;
129
130 bool widthAdjusted = false;
131 bool heightAdjusted = false;
132
133 const QQuickItem *centerInParent = p->anchors ? p->getAnchors()->centerIn() : nullptr;
134 const QQuickOverlay *centerInOverlay = qobject_cast<const QQuickOverlay*>(object: centerInParent);
135 QRectF rect(!centerInParent ? p->allowHorizontalMove ? p->x : popupItem->x() : 0,
136 !centerInParent ? p->allowVerticalMove ? p->y : popupItem->y() : 0,
137 !p->hasWidth && iw > 0 ? iw : w, !p->hasHeight && ih > 0 ? ih : h);
138 bool relaxEdgeConstraint = p->relaxEdgeConstraint;
139 if (m_parentItem) {
140 // m_parentItem is the parent that the popup should open in,
141 // and popupItem()->parentItem() is the overlay, so the mapToItem() calls below
142 // effectively map the rect to scene coordinates.
143
144 // Animations can cause reposition() to get called when m_parentItem no longer has a window.
145 if (!m_parentItem->window())
146 return;
147
148 if (centerInParent) {
149 if (centerInParent != parentItem() && !centerInOverlay) {
150 qmlWarning(me: m_popup) << "Popup can only be centered within its immediate parent or Overlay.overlay";
151 return;
152 }
153
154 if (centerInOverlay) {
155 rect.moveCenter(p: QPointF(qRound(d: centerInOverlay->width() / 2.0), qRound(d: centerInOverlay->height() / 2.0)));
156 // Popup cannot be moved outside window bounds when its centered with overlay
157 relaxEdgeConstraint = false;
158 } else {
159 const QPointF parentItemCenter = QPointF(qRound(d: m_parentItem->width() / 2), qRound(d: m_parentItem->height() / 2));
160 rect.moveCenter(p: m_parentItem->mapToItem(item: popupItem->parentItem(), point: parentItemCenter));
161 }
162 } else {
163 rect.moveTopLeft(p: m_parentItem->mapToItem(item: popupItem->parentItem(), point: rect.topLeft()));
164 }
165
166 // The overlay is assumed to fully cover the window's contents, although the overlay's geometry
167 // might not always equal the window's geometry (for example, if the window's contents are rotated).
168 QQuickOverlay *overlay = QQuickOverlay::overlay(window: p->window);
169 if (overlay) {
170 qreal boundsWidth = overlay->width();
171 qreal boundsHeight = overlay->height();
172
173 // QTBUG-126843: On some platforms, the overlay's geometry is not yet available at the instant
174 // when Component.completed() is emitted. Fall back to the window's geometry for this edge case.
175 if (Q_UNLIKELY(boundsWidth <= 0)) {
176 boundsWidth = p->window->width();
177 boundsHeight = p->window->height();
178 }
179
180 const QMarginsF margins = p->getMargins();
181 QRectF bounds(qMax<qreal>(a: 0.0, b: margins.left()),
182 qMax<qreal>(a: 0.0, b: margins.top()),
183 boundsWidth - qMax<qreal>(a: 0.0, b: margins.left()) - qMax<qreal>(a: 0.0, b: margins.right()),
184 boundsHeight - qMax<qreal>(a: 0.0, b: margins.top()) - qMax<qreal>(a: 0.0, b: margins.bottom()));
185
186 // if the popup doesn't fit horizontally inside the window, try flipping it around (left <-> right)
187 if (p->allowHorizontalFlip && (rect.left() < bounds.left() || rect.right() > bounds.right())) {
188 const QPointF newTopLeft(m_parentItem->width() - p->x - rect.width(), p->y);
189 const QRectF flipped(m_parentItem->mapToItem(item: popupItem->parentItem(), point: newTopLeft),
190 rect.size());
191 if (flipped.intersected(r: bounds).width() > rect.intersected(r: bounds).width())
192 rect.moveLeft(pos: flipped.left());
193 }
194
195 // if the popup doesn't fit vertically inside the window, try flipping it around (above <-> below)
196 if (p->allowVerticalFlip && (rect.top() < bounds.top() || rect.bottom() > bounds.bottom())) {
197 const QPointF newTopLeft(p->x, m_parentItem->height() - p->y - rect.height());
198 const QRectF flipped(m_parentItem->mapToItem(item: popupItem->parentItem(), point: newTopLeft),
199 rect.size());
200 if (flipped.intersected(r: bounds).height() > rect.intersected(r: bounds).height())
201 rect.moveTop(pos: flipped.top());
202 }
203
204 // push inside the margins if specified
205 if (p->allowVerticalMove) {
206 if (margins.top() >= 0 && rect.top() < bounds.top())
207 rect.moveTop(pos: margins.top());
208 if (margins.bottom() >= 0 && rect.bottom() > bounds.bottom())
209 rect.moveBottom(pos: bounds.bottom());
210 }
211 if (p->allowHorizontalMove) {
212 if (margins.left() >= 0 && rect.left() < bounds.left())
213 rect.moveLeft(pos: margins.left());
214 if (margins.right() >= 0 && rect.right() > bounds.right())
215 rect.moveRight(pos: bounds.right());
216 }
217
218 if (iw > 0 && (rect.left() < bounds.left() || rect.right() > bounds.right())) {
219 // neither the flipped or pushed geometry fits inside the window, choose
220 // whichever side (left vs. right) fits larger part of the popup
221 if (p->allowHorizontalMove && p->allowHorizontalFlip) {
222 if (rect.left() < bounds.left() && bounds.left() + rect.width() <= bounds.right())
223 rect.moveLeft(pos: bounds.left());
224 else if (rect.right() > bounds.right() && bounds.right() - rect.width() >= bounds.left())
225 rect.moveRight(pos: bounds.right());
226 }
227
228 // as a last resort, adjust the width to fit the window
229 // Negative margins don't require resize as popup not pushed within
230 // the boundary. But otherwise, retain existing behavior of resizing
231 // for items, such as menus, which enables flip.
232 if (p->allowHorizontalResize) {
233 if ((margins.left() >= 0 || !relaxEdgeConstraint)
234 && (rect.left() < bounds.left())) {
235 rect.setLeft(bounds.left());
236 widthAdjusted = true;
237 }
238 if ((margins.right() >= 0 || !relaxEdgeConstraint)
239 && (rect.right() > bounds.right())) {
240 rect.setRight(bounds.right());
241 widthAdjusted = true;
242 }
243 }
244 } else if (iw > 0 && rect.left() >= bounds.left() && rect.right() <= bounds.right()
245 && iw != w) {
246 // restore original width
247 rect.setWidth(iw);
248 widthAdjusted = true;
249 }
250
251 if (ih > 0 && (rect.top() < bounds.top() || rect.bottom() > bounds.bottom())) {
252 // neither the flipped or pushed geometry fits inside the window, choose
253 // whichever side (above vs. below) fits larger part of the popup
254 if (p->allowVerticalMove && p->allowVerticalFlip) {
255 if (rect.top() < bounds.top() && bounds.top() + rect.height() <= bounds.bottom())
256 rect.moveTop(pos: bounds.top());
257 else if (rect.bottom() > bounds.bottom() && bounds.bottom() - rect.height() >= bounds.top())
258 rect.moveBottom(pos: bounds.bottom());
259 }
260
261 // as a last resort, adjust the height to fit the window
262 // Negative margins don't require resize as popup not pushed within
263 // the boundary. But otherwise, retain existing behavior of resizing
264 // for items, such as menus, which enables flip.
265 if (p->allowVerticalResize) {
266 if ((margins.top() >= 0 || !relaxEdgeConstraint)
267 && (rect.top() < bounds.top())) {
268 rect.setTop(bounds.top());
269 heightAdjusted = true;
270 }
271 if ((margins.bottom() >= 0 || !relaxEdgeConstraint)
272 && (rect.bottom() > bounds.bottom())) {
273 rect.setBottom(bounds.bottom());
274 heightAdjusted = true;
275 }
276 }
277 } else if (ih > 0 && rect.top() >= bounds.top() && rect.bottom() <= bounds.bottom()
278 && ih != h) {
279 // restore original height
280 rect.setHeight(ih);
281 heightAdjusted = true;
282 }
283 }
284 }
285
286 m_positioning = true;
287
288 const QPointF windowPos = rect.topLeft();
289 popupItem->setPosition(windowPos);
290
291 // If the popup was assigned a parent, rect will be in scene coordinates,
292 // so we need to map its top left back to item coordinates.
293 // However, if centering within the overlay, the coordinates will be relative
294 // to the window, so we don't need to do anything.
295 // The same applies to popups that are in their own dedicated window.
296 if (m_parentItem && !centerInOverlay)
297 p->setEffectivePosFromWindowPos(m_parentItem->mapFromScene(point: windowPos));
298 else
299 p->setEffectivePosFromWindowPos(windowPos);
300
301 if (!p->hasWidth && widthAdjusted && rect.width() > 0) {
302 popupItem->setWidth(rect.width() / m_popupScale);
303 // The popup doesn't have an explicit width, so we should respect that by not
304 // making our call above an explicit assignment. If we don't, the popup won't
305 // resize after being repositioned in some cases.
306 QQuickItemPrivate::get(item: popupItem)->widthValidFlag = false;
307 }
308 if (!p->hasHeight && heightAdjusted && rect.height() > 0) {
309 popupItem->setHeight(rect.height() / m_popupScale);
310 QQuickItemPrivate::get(item: popupItem)->heightValidFlag = false;
311 }
312 m_positioning = false;
313
314 qCDebug(lcPopupPositioner) << "- new popupItem geometry:"
315 << popupItem->x() << popupItem->y() << popupItem->width() << popupItem->height();
316}
317
318void QQuickPopupPositioner::itemGeometryChanged(QQuickItem *, QQuickGeometryChange, const QRectF &)
319{
320 if (m_parentItem && m_popup->popupItem()->isVisible())
321 QQuickPopupPrivate::get(popup: m_popup)->reposition();
322}
323
324void QQuickPopupPositioner::itemParentChanged(QQuickItem *, QQuickItem *parent)
325{
326 addAncestorListeners(item: parent);
327}
328
329void QQuickPopupPositioner::itemChildRemoved(QQuickItem *item, QQuickItem *child)
330{
331 if (child == m_parentItem || child->isAncestorOf(child: m_parentItem))
332 removeAncestorListeners(item);
333}
334
335void QQuickPopupPositioner::removeAncestorListeners(QQuickItem *item)
336{
337 if (item == m_parentItem)
338 return;
339
340 QQuickItem *p = item;
341 while (p) {
342 QQuickItemPrivate::get(item: p)->removeItemChangeListener(this, types: AncestorChangeTypes);
343 p = p->parentItem();
344 }
345}
346
347void QQuickPopupPositioner::addAncestorListeners(QQuickItem *item)
348{
349 if (item == m_parentItem)
350 return;
351
352 QQuickItem *p = item;
353 while (p) {
354 QQuickItemPrivate::get(item: p)->updateOrAddItemChangeListener(listener: this, types: AncestorChangeTypes);
355 p = p->parentItem();
356 }
357}
358
359QT_END_NAMESPACE
360

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

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