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 "qquickpopup_p_p.h"
9
10#include <QtCore/qloggingcategory.h>
11#include <QtQml/qqmlinfo.h>
12#include <QtQuick/private/qquickitem_p.h>
13
14QT_BEGIN_NAMESPACE
15
16Q_LOGGING_CATEGORY(lcPopupPositioner, "qt.quick.controls.popuppositioner")
17
18static const QQuickItemPrivate::ChangeTypes AncestorChangeTypes = QQuickItemPrivate::Geometry
19 | QQuickItemPrivate::Parent
20 | QQuickItemPrivate::Children;
21
22static const QQuickItemPrivate::ChangeTypes ItemChangeTypes = QQuickItemPrivate::Geometry
23 | QQuickItemPrivate::Parent;
24
25QQuickPopupPositioner::QQuickPopupPositioner(QQuickPopup *popup)
26 : m_popup(popup)
27{
28}
29
30QQuickPopupPositioner::~QQuickPopupPositioner()
31{
32 if (m_parentItem) {
33 QQuickItemPrivate::get(item: m_parentItem)->removeItemChangeListener(this, types: ItemChangeTypes);
34 removeAncestorListeners(item: m_parentItem->parentItem());
35 }
36}
37
38QQuickPopup *QQuickPopupPositioner::popup() const
39{
40 return m_popup;
41}
42
43QQuickItem *QQuickPopupPositioner::parentItem() const
44{
45 return m_parentItem;
46}
47
48void QQuickPopupPositioner::setParentItem(QQuickItem *parent)
49{
50 if (m_parentItem == parent)
51 return;
52
53 if (m_parentItem) {
54 QQuickItemPrivate::get(item: m_parentItem)->removeItemChangeListener(this, types: ItemChangeTypes);
55 removeAncestorListeners(item: m_parentItem->parentItem());
56 }
57
58 m_parentItem = parent;
59
60 if (!parent)
61 return;
62
63 QQuickItemPrivate::get(item: parent)->addItemChangeListener(listener: this, types: ItemChangeTypes);
64 addAncestorListeners(item: parent->parentItem());
65 // Store the scale property so the end result of any transition that could effect the scale
66 // does not influence the top left of the final popup, so it doesn't appear to flip from one
67 // position to another as a result
68 m_popupScale = m_popup->popupItem()->scale();
69 if (m_popup->popupItem()->isVisible())
70 QQuickPopupPrivate::get(popup: m_popup)->reposition();
71}
72
73void QQuickPopupPositioner::reposition()
74{
75 QQuickItem *popupItem = m_popup->popupItem();
76 if (!popupItem->isVisible())
77 return;
78
79 if (m_positioning) {
80 popupItem->polish();
81 return;
82 }
83
84 qCDebug(lcPopupPositioner) << "reposition called for" << m_popup;
85
86 const qreal w = popupItem->width() * m_popupScale;
87 const qreal h = popupItem->height() * m_popupScale;
88 const qreal iw = popupItem->implicitWidth() * m_popupScale;
89 const qreal ih = popupItem->implicitHeight() * m_popupScale;
90
91 bool widthAdjusted = false;
92 bool heightAdjusted = false;
93 QQuickPopupPrivate *p = QQuickPopupPrivate::get(popup: m_popup);
94
95 const QQuickItem *centerInParent = p->anchors ? p->getAnchors()->centerIn() : nullptr;
96 const QQuickOverlay *centerInOverlay = qobject_cast<const QQuickOverlay*>(object: centerInParent);
97 QRectF rect(!centerInParent ? p->allowHorizontalMove ? p->x : popupItem->x() : 0,
98 !centerInParent ? p->allowVerticalMove ? p->y : popupItem->y() : 0,
99 !p->hasWidth && iw > 0 ? iw : w,
100 !p->hasHeight && ih > 0 ? ih : h);
101 if (m_parentItem) {
102 // m_parentItem is the parent that the popup should open in,
103 // and popupItem()->parentItem() is the overlay, so the mapToItem() calls below
104 // effectively map the rect to scene coordinates.
105 if (centerInParent) {
106 if (centerInParent != parentItem() && !centerInOverlay) {
107 qmlWarning(me: m_popup) << "Popup can only be centered within its immediate parent or Overlay.overlay";
108 return;
109 }
110
111 if (centerInOverlay) {
112 rect.moveCenter(p: QPointF(qRound(d: centerInOverlay->width() / 2.0), qRound(d: centerInOverlay->height() / 2.0)));
113 } else {
114 const QPointF parentItemCenter = QPointF(qRound(d: m_parentItem->width() / 2), qRound(d: m_parentItem->height() / 2));
115 rect.moveCenter(p: m_parentItem->mapToItem(item: popupItem->parentItem(), point: parentItemCenter));
116 }
117 } else {
118 rect.moveTopLeft(p: m_parentItem->mapToItem(item: popupItem->parentItem(), point: rect.topLeft()));
119 }
120
121 if (p->window) {
122 const QMarginsF margins = p->getMargins();
123 QRectF bounds(qMax<qreal>(a: 0.0, b: margins.left()),
124 qMax<qreal>(a: 0.0, b: margins.top()),
125 p->window->width() - qMax<qreal>(a: 0.0, b: margins.left()) - qMax<qreal>(a: 0.0, b: margins.right()),
126 p->window->height() - qMax<qreal>(a: 0.0, b: margins.top()) - qMax<qreal>(a: 0.0, b: margins.bottom()));
127 if (p->window->contentOrientation() == Qt::LandscapeOrientation || p->window->contentOrientation() == Qt::InvertedLandscapeOrientation)
128 bounds = bounds.transposed();
129
130 // if the popup doesn't fit horizontally inside the window, try flipping it around (left <-> right)
131 if (p->allowHorizontalFlip && (rect.left() < bounds.left() || rect.right() > bounds.right())) {
132 const QPointF newTopLeft(m_parentItem->width() - p->x - rect.width(), p->y);
133 const QRectF flipped(m_parentItem->mapToItem(item: popupItem->parentItem(), point: newTopLeft),
134 rect.size());
135 if (flipped.intersected(r: bounds).width() > rect.intersected(r: bounds).width())
136 rect.moveLeft(pos: flipped.left());
137 }
138
139 // if the popup doesn't fit vertically inside the window, try flipping it around (above <-> below)
140 if (p->allowVerticalFlip && (rect.top() < bounds.top() || rect.bottom() > bounds.bottom())) {
141 const QPointF newTopLeft(p->x, m_parentItem->height() - p->y - rect.height());
142 const QRectF flipped(m_parentItem->mapToItem(item: popupItem->parentItem(), point: newTopLeft),
143 rect.size());
144 if (flipped.intersected(r: bounds).height() > rect.intersected(r: bounds).height())
145 rect.moveTop(pos: flipped.top());
146 }
147
148 // push inside the margins if specified
149 if (p->allowVerticalMove) {
150 if (margins.top() >= 0 && rect.top() < bounds.top())
151 rect.moveTop(pos: margins.top());
152 if (margins.bottom() >= 0 && rect.bottom() > bounds.bottom())
153 rect.moveBottom(pos: bounds.bottom());
154 }
155 if (p->allowHorizontalMove) {
156 if (margins.left() >= 0 && rect.left() < bounds.left())
157 rect.moveLeft(pos: margins.left());
158 if (margins.right() >= 0 && rect.right() > bounds.right())
159 rect.moveRight(pos: bounds.right());
160 }
161
162 if (iw > 0 && (rect.left() < bounds.left() || rect.right() > bounds.right())) {
163 // neither the flipped or pushed geometry fits inside the window, choose
164 // whichever side (left vs. right) fits larger part of the popup
165 if (p->allowHorizontalMove && p->allowHorizontalFlip) {
166 if (rect.left() < bounds.left() && bounds.left() + rect.width() <= bounds.right())
167 rect.moveLeft(pos: bounds.left());
168 else if (rect.right() > bounds.right() && bounds.right() - rect.width() >= bounds.left())
169 rect.moveRight(pos: bounds.right());
170 }
171
172 // as a last resort, adjust the width to fit the window
173 if (p->allowHorizontalResize) {
174 if (rect.left() < bounds.left()) {
175 rect.setLeft(bounds.left());
176 widthAdjusted = true;
177 }
178 if (rect.right() > bounds.right()) {
179 rect.setRight(bounds.right());
180 widthAdjusted = true;
181 }
182 }
183 } else if (iw > 0 && rect.left() >= bounds.left() && rect.right() <= bounds.right()
184 && iw != w) {
185 // restore original width
186 rect.setWidth(iw);
187 widthAdjusted = true;
188 }
189
190 if (ih > 0 && (rect.top() < bounds.top() || rect.bottom() > bounds.bottom())) {
191 // neither the flipped or pushed geometry fits inside the window, choose
192 // whichever side (above vs. below) fits larger part of the popup
193 if (p->allowVerticalMove && p->allowVerticalFlip) {
194 if (rect.top() < bounds.top() && bounds.top() + rect.height() <= bounds.bottom())
195 rect.moveTop(pos: bounds.top());
196 else if (rect.bottom() > bounds.bottom() && bounds.bottom() - rect.height() >= bounds.top())
197 rect.moveBottom(pos: bounds.bottom());
198 }
199
200 // as a last resort, adjust the height to fit the window
201 if (p->allowVerticalResize) {
202 if (rect.top() < bounds.top()) {
203 rect.setTop(bounds.top());
204 heightAdjusted = true;
205 }
206 if (rect.bottom() > bounds.bottom()) {
207 rect.setBottom(bounds.bottom());
208 heightAdjusted = true;
209 }
210 }
211 } else if (ih > 0 && rect.top() >= bounds.top() && rect.bottom() <= bounds.bottom()
212 && ih != h) {
213 // restore original height
214 rect.setHeight(ih);
215 heightAdjusted = true;
216 }
217 }
218 }
219
220 m_positioning = true;
221
222 popupItem->setPosition(rect.topLeft());
223
224 // If the popup was assigned a parent, rect will be in scene coordinates,
225 // so we need to map its top left back to item coordinates.
226 // However, if centering within the overlay, the coordinates will be relative
227 // to the window, so we don't need to do anything.
228 const QPointF effectivePos = m_parentItem && !centerInOverlay ? m_parentItem->mapFromScene(point: rect.topLeft()) : rect.topLeft();
229 if (!qFuzzyCompare(p1: p->effectiveX, p2: effectivePos.x())) {
230 p->effectiveX = effectivePos.x();
231 emit m_popup->xChanged();
232 }
233 if (!qFuzzyCompare(p1: p->effectiveY, p2: effectivePos.y())) {
234 p->effectiveY = effectivePos.y();
235 emit m_popup->yChanged();
236 }
237
238 if (!p->hasWidth && widthAdjusted && rect.width() > 0) {
239 popupItem->setWidth(rect.width() / m_popupScale);
240 // The popup doesn't have an explicit width, so we should respect that by not
241 // making our call above an explicit assignment. If we don't, the popup won't
242 // resize after being repositioned in some cases.
243 QQuickItemPrivate::get(item: popupItem)->widthValidFlag = false;
244 }
245 if (!p->hasHeight && heightAdjusted && rect.height() > 0) {
246 popupItem->setHeight(rect.height() / m_popupScale);
247 QQuickItemPrivate::get(item: popupItem)->heightValidFlag = false;
248 }
249 m_positioning = false;
250
251 qCDebug(lcPopupPositioner) << "- new popupItem geometry:"
252 << popupItem->x() << popupItem->y() << popupItem->width() << popupItem->height();
253}
254
255void QQuickPopupPositioner::itemGeometryChanged(QQuickItem *, QQuickGeometryChange, const QRectF &)
256{
257 if (m_parentItem && m_popup->popupItem()->isVisible())
258 QQuickPopupPrivate::get(popup: m_popup)->reposition();
259}
260
261void QQuickPopupPositioner::itemParentChanged(QQuickItem *, QQuickItem *parent)
262{
263 addAncestorListeners(item: parent);
264}
265
266void QQuickPopupPositioner::itemChildRemoved(QQuickItem *item, QQuickItem *child)
267{
268 if (child == m_parentItem || child->isAncestorOf(child: m_parentItem))
269 removeAncestorListeners(item);
270}
271
272void QQuickPopupPositioner::removeAncestorListeners(QQuickItem *item)
273{
274 if (item == m_parentItem)
275 return;
276
277 QQuickItem *p = item;
278 while (p) {
279 QQuickItemPrivate::get(item: p)->removeItemChangeListener(this, types: AncestorChangeTypes);
280 p = p->parentItem();
281 }
282}
283
284void QQuickPopupPositioner::addAncestorListeners(QQuickItem *item)
285{
286 if (item == m_parentItem)
287 return;
288
289 QQuickItem *p = item;
290 while (p) {
291 QQuickItemPrivate::get(item: p)->updateOrAddItemChangeListener(listener: this, types: AncestorChangeTypes);
292 p = p->parentItem();
293 }
294}
295
296QT_END_NAMESPACE
297

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