1// Copyright (C) 2024 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 "qquickpopupwindow_p_p.h"
5#include "qquickcombobox_p.h"
6#include "qquickdialog_p.h"
7#include "qquickpopup_p.h"
8#include "qquickpopup_p_p.h"
9#include "qquickmenu_p_p.h"
10#include "qquickmenubar_p_p.h"
11#include "qquickpopupitem_p_p.h"
12#include <QtGui/private/qguiapplication_p.h>
13
14#include <QtCore/qloggingcategory.h>
15#include <QtGui/private/qeventpoint_p.h>
16#include <QtQuick/private/qquickitem_p.h>
17#include <QtQuick/private/qquickwindowmodule_p.h>
18#include <QtQuick/private/qquickwindowmodule_p_p.h>
19#include <qpa/qplatformwindow_p.h>
20
21QT_BEGIN_NAMESPACE
22
23Q_STATIC_LOGGING_CATEGORY(lcPopupWindow, "qt.quick.controls.popup.window")
24
25static bool s_popupGrabOk = false;
26static QPointer<QWindow> s_grabbedWindow;
27
28class QQuickPopupWindowPrivate : public QQuickWindowQmlImplPrivate
29{
30 Q_DECLARE_PUBLIC(QQuickPopupWindow)
31
32public:
33 QPointer<QQuickItem> m_popupItem;
34 QPointer<QQuickPopup> m_popup;
35 QPointer<QWindow> m_popupParentItemWindow;
36 bool m_inHideEvent = false;
37
38protected:
39 void setVisible(bool visible) override;
40
41private:
42 bool filterPopupSpecialCases(QEvent *event);
43};
44
45QQuickPopupWindow::QQuickPopupWindow(QQuickPopup *popup, QWindow *parent)
46 : QQuickWindowQmlImpl(*(new QQuickPopupWindowPrivate), nullptr)
47{
48 Q_D(QQuickPopupWindow);
49
50 d->m_popup = popup;
51 d->m_popupItem = popup->popupItem();
52 setTransientParent(parent);
53
54 connect(sender: d->m_popup, signal: &QQuickPopup::windowChanged, context: this, slot: &QQuickPopupWindow::windowChanged);
55 connect(sender: d->m_popup, signal: &QQuickPopup::implicitWidthChanged, context: this, slot: &QQuickPopupWindow::implicitWidthChanged);
56 connect(sender: d->m_popup, signal: &QQuickPopup::implicitHeightChanged, context: this, slot: &QQuickPopupWindow::implicitHeightChanged);
57 if (QQuickWindow *nearestParentItemWindow = d->m_popup->window()) {
58 d->m_popupParentItemWindow = nearestParentItemWindow;
59 connect(sender: d->m_popupParentItemWindow, signal: &QWindow::xChanged, context: this, slot: &QQuickPopupWindow::parentWindowXChanged);
60 connect(sender: d->m_popupParentItemWindow, signal: &QWindow::yChanged, context: this, slot: &QQuickPopupWindow::parentWindowYChanged);
61 }
62 setWidth(d->m_popupItem->implicitWidth());
63 setHeight(d->m_popupItem->implicitHeight());
64
65 const auto flags = QQuickPopupPrivate::get(popup)->popupWindowType();
66
67 // For popup windows, we'll need to draw everything, in order to have enough control over the styling.
68 if (flags & Qt::Popup)
69 setColor(QColorConstants::Transparent);
70
71 setFlags(flags);
72
73 qCDebug(lcPopupWindow) << "Created popup window with parent:" << parent << "flags:" << flags;
74}
75
76QQuickPopup *QQuickPopupWindow::popup() const
77{
78 Q_D(const QQuickPopupWindow);
79 return d->m_popup;
80}
81
82void QQuickPopupWindow::hideEvent(QHideEvent *e)
83{
84 Q_D(QQuickPopupWindow);
85 QQuickWindow::hideEvent(e);
86 // Avoid potential infinite recursion, between QWindowPrivate::setVisible(false) and this function.
87 QScopedValueRollback<bool>inHideEventRollback(d->m_inHideEvent, true);
88 if (QQuickPopup *popup = d->m_popup) {
89 QQuickDialog *dialog = qobject_cast<QQuickDialog *>(object: popup);
90 if (dialog && QQuickPopupPrivate::get(popup: dialog)->visible)
91 dialog->reject();
92 else
93 popup->setVisible(false);
94 }
95}
96
97void QQuickPopupWindow::moveEvent(QMoveEvent *e)
98{
99 handlePopupPositionChangeFromWindowSystem(pos: e->pos());
100}
101
102void QQuickPopupWindow::resizeEvent(QResizeEvent *e)
103{
104 Q_D(QQuickPopupWindow);
105 QQuickWindowQmlImpl::resizeEvent(e);
106
107 if (!d->m_popupItem)
108 return;
109
110 qCDebug(lcPopupWindow) << "A window system event changed the popup's size to be " << e->size();
111 QQuickPopupPrivate *popupPrivate = QQuickPopupPrivate::get(popup: d->m_popup);
112
113 const auto topLeftFromSystem = global2Local(pos: d->geometry.topLeft());
114 // We need to use the current topLeft position here, so that reposition()
115 // does not move the window
116 const auto oldX = popupPrivate->x;
117 const auto oldY = popupPrivate->y;
118
119 if (Q_LIKELY(topLeftFromSystem)) {
120 popupPrivate->x = topLeftFromSystem->x();
121 popupPrivate->y = topLeftFromSystem->y();
122 }
123
124 const QMarginsF windowInsets = popupPrivate->windowInsets();
125 d->m_popupItem->setWidth(e->size().width() - windowInsets.left() - windowInsets.right());
126 d->m_popupItem->setHeight(e->size().height() - windowInsets.top() - windowInsets.bottom());
127
128 // and restore the actual x and y afterwards
129 popupPrivate->x = oldX;
130 popupPrivate->y = oldY;
131}
132
133void QQuickPopupWindowPrivate::setVisible(bool visible)
134{
135 Q_Q(QQuickPopupWindow);
136 const bool isTransientParentDestroyed = !q->transientParent() ? true :
137 QQuickWindowPrivate::get(c: qobject_cast<QQuickWindow *>(object: q->transientParent()))->inDestructor;
138 if (m_inHideEvent || isTransientParentDestroyed)
139 return;
140
141 const bool visibleChanged = QWindowPrivate::visible != visible;
142
143 // Check if we're about to close the last popup, in which case, ungrab.
144 if (!visible && visibleChanged && QGuiApplicationPrivate::popupCount() == 1 && s_grabbedWindow) {
145 s_grabbedWindow->setMouseGrabEnabled(false);
146 s_grabbedWindow->setKeyboardGrabEnabled(false);
147 s_popupGrabOk = false;
148 qCDebug(lcPopupWindow) << "The window " << s_grabbedWindow << "has disabled global mouse and keyboard grabs.";
149 s_grabbedWindow = nullptr;
150 }
151
152 QQuickWindowQmlImplPrivate::setVisible(visible);
153
154 // Similar logic to grabForPopup(QWidget *popup)
155 // If the user clicks outside, popups with CloseOnPressOutside*/CloseOnReleaseOutside* need to be able to react,
156 // in order to determine if they should close.
157 // Pointer press and release events should also be filtered by the top-most popup window, and only be delivered to other windows in rare cases.
158 if (visible && visibleChanged && QGuiApplicationPrivate::popupCount() == 1 && !s_popupGrabOk) {
159 QWindow *win = m_popup->window();
160 if (QGuiApplication::platformName() == QStringLiteral("offscreen"))
161 return; // workaround for QTBUG-134009
162 s_popupGrabOk = win->setKeyboardGrabEnabled(true);
163 if (s_popupGrabOk) {
164 s_popupGrabOk = win->setMouseGrabEnabled(true);
165 if (!s_popupGrabOk)
166 win->setKeyboardGrabEnabled(false);
167 s_grabbedWindow = win;
168 qCDebug(lcPopupWindow) << "The window" << win << "has enabled global mouse" << (s_popupGrabOk ? "and keyboard" : "") << "grabs.";
169 }
170 }
171}
172
173/*! \internal
174 Even if all pointer events are sent to the active popup, there are cases
175 where we need to take several popups, or even the menu bar, into account
176 to figure out what the event should do.
177
178 - When clicking outside a popup, the closePolicy should determine whether the
179 popup should close or not. When closing a menu this way, all other menus
180 that are grouped together should also close.
181
182 - We want all open menus and sub menus that belong together to almost act as
183 a single popup WRT hover event delivery. This will allow the user to hover
184 and highlight MenuItems inside all of them, not just this menu. This function
185 will therefore find the menu, or menu bar, under the event's position, and
186 forward hover events to it.
187
188 Note that we for most cases want to return false from this function, even if
189 the event was actually handled. That way it will be also sent to the DA, to
190 let normal event delivery to any potential grabbers happen the usual way. It
191 will also allow QGuiApplication to forward the event to the window under the
192 pointer if the event was outside of any popups (if supported by e.g
193 QPlatformIntegration::ReplayMousePressOutsidePopup).
194 */
195bool QQuickPopupWindowPrivate::filterPopupSpecialCases(QEvent *event)
196{
197 Q_Q(QQuickPopupWindow);
198
199 if (!event->isPointerEvent())
200 return false;
201
202 QQuickPopup *popup = m_popup;
203 if (!popup)
204 return false;
205
206 auto *pe = static_cast<QPointerEvent *>(event);
207 const QPointF globalPos = pe->points().first().globalPosition();
208 const QQuickPopup::ClosePolicy closePolicy = popup->closePolicy();
209 QQuickPopup *targetPopup = QQuickPopupPrivate::get(popup)->contains(scenePos: contentItem->mapFromGlobal(point: globalPos)) ? popup : nullptr;
210
211 // Resolve the Menu or MenuBar under the mouse, if any
212 QQuickMenu *menu = qobject_cast<QQuickMenu *>(object: popup);
213 QQuickMenuBar *targetMenuBar = nullptr;
214 QObject *menuParent = menu;
215 while (menuParent) {
216 if (auto *parentMenu = qobject_cast<QQuickMenu *>(object: menuParent)) {
217 QQuickPopupWindow *popupWindow = QQuickMenuPrivate::get(menu: parentMenu)->popupWindow;
218 auto *popup_d = QQuickPopupPrivate::get(popup: popupWindow->popup());
219 QPointF scenePos = popupWindow->contentItem()->mapFromGlobal(point: globalPos);
220 if (popup_d->contains(scenePos)) {
221 targetPopup = parentMenu;
222 break;
223 }
224 } else if (auto *menuBar = qobject_cast<QQuickMenuBar *>(object: menuParent)) {
225 const QPointF menuBarPos = menuBar->mapFromGlobal(point: globalPos);
226 if (menuBar->contains(point: menuBarPos))
227 targetMenuBar = menuBar;
228 break;
229 }
230
231 menuParent = menuParent->parent();
232 }
233
234 auto closePopupAndParentMenus = [q]() {
235 QQuickPopup *current = q->popup();
236 do {
237 qCDebug(lcPopupWindow) << "Closing" << current << "from an outside pointer press or release event";
238 current->close();
239 current = qobject_cast<QQuickMenu *>(object: current->parent());
240 } while (current);
241 };
242
243 if (pe->isBeginEvent()) {
244 if (targetMenuBar) {
245 // If the press was on top of the menu bar, we close all menus and return
246 // true. The latter will stop QGuiApplication from propagating the event
247 // to the window under the pointer, and therefore also to the MenuBar.
248 // The latter would otherwise cause a menu to reopen again immediately, and
249 // undermine that we want to close all popups.
250 closePopupAndParentMenus();
251 return true;
252 } else if (!targetPopup && closePolicy.testAnyFlags(flags: QQuickPopup::CloseOnPressOutside | QQuickPopup::CloseOnPressOutsideParent)) {
253 // Pressed outside either a popup window, or a menu or menubar that owns a menu using popup windows.
254 // Note that A QQuickPopupWindow can be bigger than the
255 // menu itself, to make room for a drop-shadow. But if the press was on top
256 // of the shadow, targetMenu will still be nullptr.
257 closePopupAndParentMenus();
258 return false;
259 }
260 } else if (pe->isUpdateEvent()){
261 QQuickWindow *targetWindow = nullptr;
262 if (targetPopup)
263 targetWindow = QQuickPopupPrivate::get(popup: targetPopup)->popupWindow;
264 else if (targetMenuBar)
265 targetWindow = targetMenuBar->window();
266 else
267 return false;
268
269 // Forward move events to the target window
270 const auto scenePos = pe->point(i: 0).scenePosition();
271 const auto translatedScenePos = targetWindow->mapFromGlobal(pos: globalPos);
272 QMutableEventPoint::setScenePosition(p&: pe->point(i: 0), arg: translatedScenePos);
273 auto *grabber = pe->exclusiveGrabber(point: pe->point(i: 0));
274
275 if (grabber) {
276 // Temporarily disable the grabber, to stop the delivery agent inside
277 // targetWindow from forwarding the event to an item outside the menu
278 // or menubar. This is especially important to support a press on e.g
279 // a MenuBarItem, followed by a drag-and-release on top of a MenuItem.
280 pe->setExclusiveGrabber(point: pe->point(i: 0), exclusiveGrabber: nullptr);
281 }
282
283 qCDebug(lcPopupWindow) << "forwarding" << pe << "to popup menu:" << targetWindow;
284 QQuickWindowPrivate::get(c: targetWindow)->deliveryAgent->event(ev: pe);
285
286 // Restore the event before we return
287 QMutableEventPoint::setScenePosition(p&: pe->point(i: 0), arg: scenePos);
288 if (grabber)
289 pe->setExclusiveGrabber(point: pe->point(i: 0), exclusiveGrabber: grabber);
290 } else if (pe->isEndEvent()) {
291 if (!targetPopup && !targetMenuBar && closePolicy.testAnyFlags(flags: QQuickPopup::CloseOnReleaseOutside | QQuickPopup::CloseOnReleaseOutsideParent)) {
292 // Released outside either a popup window, or a menu or menubar that owns a menu using popup windows.
293 closePopupAndParentMenus();
294 return false;
295 }
296
297 // To support opening a Menu on press (e.g on a MenuBarItem), followed by
298 // a drag and release on a MenuItem inside the Menu, we ask the Menu to
299 // perform a click on the active MenuItem, if any.
300 if (QQuickMenu *targetMenu = qobject_cast<QQuickMenu *>(object: targetPopup)) {
301 qCDebug(lcPopupWindow) << "forwarding" << pe << "to popup menu:" << targetMenu;
302 QQuickMenuPrivate::get(menu: targetMenu)->handleReleaseWithoutGrab(eventPoint: pe->point(i: 0));
303 }
304 }
305
306 return false;
307}
308
309bool QQuickPopupWindow::event(QEvent *e)
310{
311 Q_D(QQuickPopupWindow);
312 if (d->filterPopupSpecialCases(event: e))
313 return true;
314
315 if (QQuickPopup *popup = d->m_popup) {
316 // Popups without focus should not consume keyboard events.
317 if (!popup->hasFocus() && (e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease)
318#if QT_CONFIG(shortcut)
319 && (!static_cast<QKeyEvent *>(e)->matches(key: QKeySequence::Cancel)
320#if defined(Q_OS_ANDROID)
321 || static_cast<QKeyEvent *>(e)->key() != Qt::Key_Back
322#endif
323 )
324#endif
325 ) return false;
326 }
327
328 return QQuickWindowQmlImpl::event(e);
329}
330
331void QQuickPopupWindow::windowChanged(QWindow *window)
332{
333 Q_D(QQuickPopupWindow);
334 if (!d->m_popupParentItemWindow.isNull()) {
335 disconnect(sender: d->m_popupParentItemWindow, signal: &QWindow::xChanged, receiver: this, slot: &QQuickPopupWindow::parentWindowXChanged);
336 disconnect(sender: d->m_popupParentItemWindow, signal: &QWindow::yChanged, receiver: this, slot: &QQuickPopupWindow::parentWindowYChanged);
337 }
338 if (window) {
339 d->m_popupParentItemWindow = window;
340 connect(sender: window, signal: &QWindow::xChanged, context: this, slot: &QQuickPopupWindow::parentWindowXChanged);
341 connect(sender: window, signal: &QWindow::yChanged, context: this, slot: &QQuickPopupWindow::parentWindowYChanged);
342 } else {
343 d->m_popupParentItemWindow.clear();
344 }
345}
346
347std::optional<QPoint> QQuickPopupWindow::global2Local(const QPoint &pos) const
348{
349 Q_D(const QQuickPopupWindow);
350 QQuickPopup *popup = d->m_popup;
351 Q_ASSERT(popup);
352 QWindow *mainWindow = d->m_popupParentItemWindow;
353 if (!mainWindow)
354 mainWindow = transientParent();
355 if (Q_UNLIKELY((!mainWindow || mainWindow != popup->window())))
356 return std::nullopt;
357
358 const QPoint scenePos = mainWindow->mapFromGlobal(pos);
359 // Popup's coordinates are relative to the nearest parent item.
360 return popup->parentItem() ? popup->parentItem()->mapFromScene(point: scenePos).toPoint() : scenePos;
361}
362
363void QQuickPopupWindow::parentWindowXChanged(int newX)
364{
365 const auto popupLocalPos = global2Local(pos: {x(), y()});
366 if (Q_UNLIKELY(!popupLocalPos))
367 return;
368 handlePopupPositionChangeFromWindowSystem(pos: { newX + popupLocalPos->x(), y() });
369}
370
371void QQuickPopupWindow::parentWindowYChanged(int newY)
372{
373 const auto popupLocalPos = global2Local(pos: {x(), y()});
374 if (Q_UNLIKELY(!popupLocalPos))
375 return;
376 handlePopupPositionChangeFromWindowSystem(pos: { x(), newY + popupLocalPos->y() });
377}
378
379void QQuickPopupWindow::handlePopupPositionChangeFromWindowSystem(const QPoint &pos)
380{
381 Q_D(QQuickPopupWindow);
382 QQuickPopup *popup = d->m_popup;
383 if (!popup)
384 return;
385
386 const auto windowPos = global2Local(pos);
387 if (Q_LIKELY(windowPos)) {
388 qCDebug(lcPopupWindow) << "A window system event changed the popup's position to be " << *windowPos;
389 QQuickPopupPrivate::get(popup)->setEffectivePosFromWindowPos(*windowPos);
390 }
391}
392
393void QQuickPopupWindow::implicitWidthChanged()
394{
395 Q_D(const QQuickPopupWindow);
396 if (auto popup = d->m_popup)
397 setWidth(popup->implicitWidth());
398}
399
400void QQuickPopupWindow::implicitHeightChanged()
401{
402 Q_D(const QQuickPopupWindow);
403 if (auto popup = d->m_popup)
404 setHeight(popup->implicitHeight());
405}
406
407QT_END_NAMESPACE
408
409

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