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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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