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