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_LOGGING_CATEGORY(lcPopupWindow, "qt.quick.controls.popup.window")
24
25static bool s_popupGrabOk = false;
26static QWindow *s_grabbedWindow = nullptr;
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 if (m_inHideEvent)
136 return;
137
138 const bool visibleChanged = QWindowPrivate::visible != visible;
139
140 // Check if we're about to close the last popup, in which case, ungrab.
141 if (!visible && visibleChanged && QGuiApplicationPrivate::popupCount() == 1 && s_grabbedWindow) {
142 s_grabbedWindow->setMouseGrabEnabled(false);
143 s_grabbedWindow->setKeyboardGrabEnabled(false);
144 s_popupGrabOk = false;
145 qCDebug(lcPopupWindow) << "The window " << s_grabbedWindow << "has disabled global mouse and keyboard grabs.";
146 s_grabbedWindow = nullptr;
147 }
148
149 QQuickWindowQmlImplPrivate::setVisible(visible);
150
151 // Similar logic to grabForPopup(QWidget *popup)
152 // If the user clicks outside, popups with CloseOnPressOutside*/CloseOnReleaseOutside* need to be able to react,
153 // in order to determine if they should close.
154 // 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.
155 if (visible && visibleChanged && QGuiApplicationPrivate::popupCount() == 1 && !s_popupGrabOk) {
156 QWindow *win = m_popup->window();
157 s_popupGrabOk = win->setKeyboardGrabEnabled(true);
158 if (s_popupGrabOk) {
159 s_popupGrabOk = win->setMouseGrabEnabled(true);
160 if (!s_popupGrabOk)
161 win->setKeyboardGrabEnabled(false);
162 s_grabbedWindow = win;
163 qCDebug(lcPopupWindow) << "The window" << win << "has enabled global mouse" << (s_popupGrabOk ? "and keyboard" : "") << "grabs.";
164 }
165 }
166}
167
168/*! \internal
169 Even if all pointer events are sent to the active popup, there are cases
170 where we need to take several popups, or even the menu bar, into account
171 to figure out what the event should do.
172
173 - When clicking outside a popup, the closePolicy should determine whether the
174 popup should close or not. When closing a menu this way, all other menus
175 that are grouped together should also close.
176
177 - We want all open menus and sub menus that belong together to almost act as
178 a single popup WRT hover event delivery. This will allow the user to hover
179 and highlight MenuItems inside all of them, not just this menu. This function
180 will therefore find the menu, or menu bar, under the event's position, and
181 forward hover events to it.
182
183 Note that we for most cases want to return false from this function, even if
184 the event was actually handled. That way it will be also sent to the DA, to
185 let normal event delivery to any potential grabbers happen the usual way. It
186 will also allow QGuiApplication to forward the event to the window under the
187 pointer if the event was outside of any popups (if supported by e.g
188 QPlatformIntegration::ReplayMousePressOutsidePopup).
189 */
190bool QQuickPopupWindowPrivate::filterPopupSpecialCases(QEvent *event)
191{
192 Q_Q(QQuickPopupWindow);
193
194 if (!event->isPointerEvent())
195 return false;
196
197 QQuickPopup *popup = m_popup;
198 if (!popup)
199 return false;
200
201 auto *pe = static_cast<QPointerEvent *>(event);
202 const QPointF globalPos = pe->points().first().globalPosition();
203 const QQuickPopup::ClosePolicy closePolicy = popup->closePolicy();
204 QQuickPopup *targetPopup = QQuickPopupPrivate::get(popup)->contains(scenePos: contentItem->mapFromGlobal(point: globalPos)) ? popup : nullptr;
205
206 // Resolve the Menu or MenuBar under the mouse, if any
207 QQuickMenu *menu = qobject_cast<QQuickMenu *>(object: popup);
208 QQuickMenuBar *targetMenuBar = nullptr;
209 QObject *menuParent = menu;
210 while (menuParent) {
211 if (auto *parentMenu = qobject_cast<QQuickMenu *>(object: menuParent)) {
212 QQuickPopupWindow *popupWindow = QQuickMenuPrivate::get(menu: parentMenu)->popupWindow;
213 auto *popup_d = QQuickPopupPrivate::get(popup: popupWindow->popup());
214 QPointF scenePos = popupWindow->contentItem()->mapFromGlobal(point: globalPos);
215 if (popup_d->contains(scenePos)) {
216 targetPopup = parentMenu;
217 break;
218 }
219 } else if (auto *menuBar = qobject_cast<QQuickMenuBar *>(object: menuParent)) {
220 const QPointF menuBarPos = menuBar->mapFromGlobal(point: globalPos);
221 if (menuBar->contains(point: menuBarPos))
222 targetMenuBar = menuBar;
223 break;
224 }
225
226 menuParent = menuParent->parent();
227 }
228
229 auto closePopupAndParentMenus = [q]() {
230 QQuickPopup *current = q->popup();
231 do {
232 qCDebug(lcPopupWindow) << "Closing" << current << "from an outside pointer press or release event";
233 current->close();
234 current = qobject_cast<QQuickMenu *>(object: current->parent());
235 } while (current);
236 };
237
238 if (pe->isBeginEvent()) {
239 if (targetMenuBar) {
240 // If the press was on top of the menu bar, we close all menus and return
241 // true. The latter will stop QGuiApplication from propagating the event
242 // to the window under the pointer, and therefore also to the MenuBar.
243 // The latter would otherwise cause a menu to reopen again immediately, and
244 // undermine that we want to close all popups.
245 closePopupAndParentMenus();
246 return true;
247 } else if (!targetPopup && closePolicy.testAnyFlags(flags: QQuickPopup::CloseOnPressOutside | QQuickPopup::CloseOnPressOutsideParent)) {
248 // Pressed outside either a popup window, or a menu or menubar that owns a menu using popup windows.
249 // Note that A QQuickPopupWindow can be bigger than the
250 // menu itself, to make room for a drop-shadow. But if the press was on top
251 // of the shadow, targetMenu will still be nullptr.
252 closePopupAndParentMenus();
253 return false;
254 }
255 } else if (pe->isUpdateEvent()){
256 QQuickWindow *targetWindow = nullptr;
257 if (targetPopup)
258 targetWindow = QQuickPopupPrivate::get(popup: targetPopup)->popupWindow;
259 else if (targetMenuBar)
260 targetWindow = targetMenuBar->window();
261 else
262 return false;
263
264 // Forward move events to the target window
265 const auto scenePos = pe->point(i: 0).scenePosition();
266 const auto translatedScenePos = targetWindow->mapFromGlobal(pos: globalPos);
267 QMutableEventPoint::setScenePosition(p&: pe->point(i: 0), arg: translatedScenePos);
268 auto *grabber = pe->exclusiveGrabber(point: pe->point(i: 0));
269
270 if (grabber) {
271 // Temporarily disable the grabber, to stop the delivery agent inside
272 // targetWindow from forwarding the event to an item outside the menu
273 // or menubar. This is especially important to support a press on e.g
274 // a MenuBarItem, followed by a drag-and-release on top of a MenuItem.
275 pe->setExclusiveGrabber(point: pe->point(i: 0), exclusiveGrabber: nullptr);
276 }
277
278 qCDebug(lcPopupWindow) << "forwarding" << pe << "to popup menu:" << targetWindow;
279 QQuickWindowPrivate::get(c: targetWindow)->deliveryAgent->event(ev: pe);
280
281 // Restore the event before we return
282 QMutableEventPoint::setScenePosition(p&: pe->point(i: 0), arg: scenePos);
283 if (grabber)
284 pe->setExclusiveGrabber(point: pe->point(i: 0), exclusiveGrabber: grabber);
285 } else if (pe->isEndEvent()) {
286 if (!targetPopup && !targetMenuBar && closePolicy.testAnyFlags(flags: QQuickPopup::CloseOnReleaseOutside | QQuickPopup::CloseOnReleaseOutsideParent)) {
287 // Released outside either a popup window, or a menu or menubar that owns a menu using popup windows.
288 closePopupAndParentMenus();
289 return false;
290 }
291
292 // To support opening a Menu on press (e.g on a MenuBarItem), followed by
293 // a drag and release on a MenuItem inside the Menu, we ask the Menu to
294 // perform a click on the active MenuItem, if any.
295 if (QQuickMenu *targetMenu = qobject_cast<QQuickMenu *>(object: targetPopup))
296 QQuickMenuPrivate::get(menu: targetMenu)->handleReleaseWithoutGrab(eventPoint: pe->point(i: 0));
297 }
298
299 return false;
300}
301
302bool QQuickPopupWindow::event(QEvent *e)
303{
304 Q_D(QQuickPopupWindow);
305 if (d->filterPopupSpecialCases(event: e))
306 return true;
307
308 if (QQuickPopup *popup = d->m_popup) {
309 // Popups without focus should not consume keyboard events.
310 if (!popup->hasFocus() && (e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease)
311#if QT_CONFIG(shortcut)
312 && (!static_cast<QKeyEvent *>(e)->matches(key: QKeySequence::Cancel)
313#if defined(Q_OS_ANDROID)
314 || static_cast<QKeyEvent *>(e)->key() != Qt::Key_Back
315#endif
316 )
317#endif
318 ) return false;
319 }
320
321 return QQuickWindowQmlImpl::event(e);
322}
323
324void QQuickPopupWindow::windowChanged(QWindow *window)
325{
326 Q_D(QQuickPopupWindow);
327 if (!d->m_popupParentItemWindow.isNull()) {
328 disconnect(sender: d->m_popupParentItemWindow, signal: &QWindow::xChanged, receiver: this, slot: &QQuickPopupWindow::parentWindowXChanged);
329 disconnect(sender: d->m_popupParentItemWindow, signal: &QWindow::yChanged, receiver: this, slot: &QQuickPopupWindow::parentWindowYChanged);
330 }
331 if (window) {
332 d->m_popupParentItemWindow = window;
333 connect(sender: window, signal: &QWindow::xChanged, context: this, slot: &QQuickPopupWindow::parentWindowXChanged);
334 connect(sender: window, signal: &QWindow::yChanged, context: this, slot: &QQuickPopupWindow::parentWindowYChanged);
335 } else {
336 d->m_popupParentItemWindow.clear();
337 }
338}
339
340std::optional<QPoint> QQuickPopupWindow::global2Local(const QPoint &pos) const
341{
342 Q_D(const QQuickPopupWindow);
343 QQuickPopup *popup = d->m_popup;
344 Q_ASSERT(popup);
345 QWindow *mainWindow = d->m_popupParentItemWindow;
346 if (!mainWindow)
347 mainWindow = transientParent();
348 if (Q_UNLIKELY((!mainWindow || mainWindow != popup->window())))
349 return std::nullopt;
350
351 const QPoint scenePos = mainWindow->mapFromGlobal(pos);
352 // Popup's coordinates are relative to the nearest parent item.
353 return popup->parentItem() ? popup->parentItem()->mapFromScene(point: scenePos).toPoint() : scenePos;
354}
355
356void QQuickPopupWindow::parentWindowXChanged(int newX)
357{
358 const auto popupLocalPos = global2Local(pos: {x(), y()});
359 if (Q_UNLIKELY(!popupLocalPos))
360 return;
361 handlePopupPositionChangeFromWindowSystem(pos: { newX + popupLocalPos->x(), y() });
362}
363
364void QQuickPopupWindow::parentWindowYChanged(int newY)
365{
366 const auto popupLocalPos = global2Local(pos: {x(), y()});
367 if (Q_UNLIKELY(!popupLocalPos))
368 return;
369 handlePopupPositionChangeFromWindowSystem(pos: { x(), newY + popupLocalPos->y() });
370}
371
372void QQuickPopupWindow::handlePopupPositionChangeFromWindowSystem(const QPoint &pos)
373{
374 Q_D(QQuickPopupWindow);
375 QQuickPopup *popup = d->m_popup;
376 if (!popup)
377 return;
378
379 const auto windowPos = global2Local(pos);
380 if (Q_LIKELY(windowPos)) {
381 qCDebug(lcPopupWindow) << "A window system event changed the popup's position to be " << *windowPos;
382 QQuickPopupPrivate::get(popup)->setEffectivePosFromWindowPos(*windowPos);
383 }
384}
385
386void QQuickPopupWindow::implicitWidthChanged()
387{
388 Q_D(const QQuickPopupWindow);
389 if (auto popup = d->m_popup)
390 setWidth(popup->implicitWidth());
391}
392
393void QQuickPopupWindow::implicitHeightChanged()
394{
395 Q_D(const QQuickPopupWindow);
396 if (auto popup = d->m_popup)
397 setHeight(popup->implicitHeight());
398}
399
400QT_END_NAMESPACE
401
402

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