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)->popupWindowFlags();
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#if QT_CONFIG(wayland)
162 // The parent control geoemtry is used by the wayland compositor when flipping menus and comboboxes.
163 if (auto waylandWindow = dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(platformWindow); waylandWindow && visible)
164 waylandWindow->setParentControlGeometry(q->parentControlGeometry());
165#endif
166
167 QQuickWindowQmlImplPrivate::setVisible(visible);
168
169 // Similar logic to grabForPopup(QWidget *popup)
170 // If the user clicks outside, popups with CloseOnPressOutside*/CloseOnReleaseOutside* need to be able to react,
171 // in order to determine if they should close.
172 // 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.
173 if (visible && visibleChanged && QGuiApplicationPrivate::popupCount() == 1 && !s_popupGrabOk) {
174 QWindow *win = m_popup->window();
175 if (QGuiApplication::platformName() == QStringLiteral("offscreen"))
176 return; // workaround for QTBUG-134009
177 s_popupGrabOk = win->setKeyboardGrabEnabled(true);
178 if (s_popupGrabOk) {
179 s_popupGrabOk = win->setMouseGrabEnabled(true);
180 if (!s_popupGrabOk)
181 win->setKeyboardGrabEnabled(false);
182 s_grabbedWindow = win;
183 qCDebug(lcPopupWindow) << "The window" << win << "has enabled global mouse" << (s_popupGrabOk ? "and keyboard" : "") << "grabs.";
184 }
185 }
186}
187
188/*! \internal
189 Even if all pointer events are sent to the active popup, there are cases
190 where we need to take several popups, or even the menu bar, into account
191 to figure out what the event should do.
192
193 - When clicking outside a popup, the closePolicy should determine whether the
194 popup should close or not. When closing a menu this way, all other menus
195 that are grouped together should also close.
196
197 - We want all open menus and sub menus that belong together to almost act as
198 a single popup WRT hover event delivery. This will allow the user to hover
199 and highlight MenuItems inside all of them, not just this menu. This function
200 will therefore find the menu, or menu bar, under the event's position, and
201 forward hover events to it.
202
203 Note that we for most cases want to return false from this function, even if
204 the event was actually handled. That way it will be also sent to the DA, to
205 let normal event delivery to any potential grabbers happen the usual way. It
206 will also allow QGuiApplication to forward the event to the window under the
207 pointer if the event was outside of any popups (if supported by e.g
208 QPlatformIntegration::ReplayMousePressOutsidePopup).
209 */
210bool QQuickPopupWindowPrivate::filterPopupSpecialCases(QEvent *event)
211{
212 Q_UNUSED(event);
213#if QT_CONFIG(quicktemplates2_container)
214 Q_Q(QQuickPopupWindow);
215
216 if (!event->isPointerEvent())
217 return false;
218
219 QQuickPopup *popup = m_popup;
220 if (!popup)
221 return false;
222
223 auto *pe = static_cast<QPointerEvent *>(event);
224 const QPointF globalPos = pe->points().first().globalPosition();
225 QQuickPopup *targetPopup = QQuickPopupPrivate::get(popup)->contains(contentItem->mapFromGlobal(globalPos)) ? popup : nullptr;
226
227 // Resolve the Menu or MenuBar under the mouse, if any
228 QQuickMenu *menu = qobject_cast<QQuickMenu *>(popup);
229 QQuickMenuBar *targetMenuBar = nullptr;
230 QObject *menuParent = menu;
231 while (menuParent) {
232 if (auto *parentMenu = qobject_cast<QQuickMenu *>(menuParent)) {
233 QQuickPopupWindow *popupWindow = QQuickMenuPrivate::get(parentMenu)->popupWindow;
234 auto *popup_d = QQuickPopupPrivate::get(popupWindow->popup());
235 QPointF scenePos = popupWindow->contentItem()->mapFromGlobal(globalPos);
236 if (popup_d->contains(scenePos)) {
237 targetPopup = parentMenu;
238 break;
239 }
240 } else if (auto *menuBar = qobject_cast<QQuickMenuBar *>(menuParent)) {
241 const QPointF menuBarPos = menuBar->mapFromGlobal(globalPos);
242 if (menuBar->contains(menuBarPos))
243 targetMenuBar = menuBar;
244 break;
245 }
246
247 menuParent = menuParent->parent();
248 }
249
250 auto closePopups = [q, globalPos](QQuickPopup::ClosePolicy closePolicy) {
251 static const QQuickPopup::ClosePolicy outsideParentFlags =
252 QQuickPopup::CloseOnPressOutsideParent | QQuickPopup::CloseOnReleaseOutsideParent;
253
254 QQuickPopup *current = q->popup();
255 while (current && current->closePolicy().testAnyFlags(closePolicy)
256 && QQuickPopupPrivate::get(current)->popupWindow) {
257 auto *currentPrivate = QQuickPopupPrivate::get(current);
258 const QPointF scenePos = currentPrivate->popupWindow->contentItem()->mapFromGlobal(globalPos);
259 if (currentPrivate->contains(scenePos))
260 break; // clicked inside the popup itself: never closes, regardless of policy
261
262 // CloseOnPress/ReleaseOutsideParent closes the popup only when the click is
263 // outside its *parent* too, not merely outside the popup itself - e.g. a click
264 // landing on a sibling/ancestor popup's own content must not close this one
265 // unless the plain Outside flag (checked above) is also what's asked for here.
266 if (current->closePolicy().testAnyFlags(closePolicy & outsideParentFlags)) {
267 const QQuickItem *parentItem = current->parentItem();
268 if (parentItem && parentItem->contains(parentItem->mapFromGlobal(globalPos)))
269 break;
270 }
271
272 qCDebug(lcPopupWindow)
273 << "Closing" << current << "from an outside pointer press or release event.";
274 current->close();
275 auto *transientParentPopupWindow = qobject_cast<QQuickPopupWindow *>(
276 currentPrivate->popupWindow->transientParent());
277 current = current->closePolicy().testAnyFlag(QQuickPopup::CloseMultiple)
278 && transientParentPopupWindow
279 ? transientParentPopupWindow->popup()
280 : nullptr;
281 }
282 };
283
284 if (pe->isBeginEvent()) {
285 if (targetMenuBar) {
286 // If the press was on top of the menu bar, we close all menus and return
287 // true. The latter will stop QGuiApplication from propagating the event
288 // to the window under the pointer, and therefore also to the MenuBar.
289 // The latter would otherwise cause a menu to reopen again immediately, and
290 // undermine that we want to close all popups.
291 closePopups(QQuickPopup::CloseOnPressOutside | QQuickPopup::CloseOnPressOutsideParent);
292 return true;
293 } else if (!targetPopup) {
294 // Pressed outside either a popup window, or a menu or menubar that owns a menu using popup windows.
295 // Note that A QQuickPopupWindow can be bigger than the
296 // menu itself, to make room for a drop-shadow. But if the press was on top
297 // of the shadow, targetMenu will still be nullptr.
298 // 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.
299 if (event->type() != QEvent::NonClientAreaMouseButtonPress && event->type() != QEvent::NonClientAreaMouseButtonDblClick) {
300 closePopups(QQuickPopup::CloseOnPressOutside | QQuickPopup::CloseOnPressOutsideParent);
301 // A modal popup must consume the press event so that forwardToPopup() sees the event as handled
302 // so that it doesn't propagate to items behind the popup window (QTBUG-131786 etc.)
303 // A QTabletEvent in particular is not accepted by default
304 if (popup->isModal()) {
305 pe->accept();
306 return true;
307 }
308 }
309 return false;
310 }
311 } else if (pe->isUpdateEvent()){
312 QQuickWindow *targetWindow = nullptr;
313 if (targetPopup)
314 targetWindow = QQuickPopupPrivate::get(targetPopup)->popupWindow;
315 else if (targetMenuBar)
316 targetWindow = targetMenuBar->window();
317 else
318 return false;
319
320 // If the target is this popup window itself, normal event delivery will
321 // handle it (e.g. an exclusive grabber like a Slider). Forwarding to the
322 // same DA with the grabber cleared would break drag operations.
323 if (targetWindow == q)
324 return false;
325
326 // Forward move events to the target window
327 const auto scenePos = pe->point(0).scenePosition();
328 const auto translatedScenePos = targetWindow->mapFromGlobal(globalPos);
329 QMutableEventPoint::setScenePosition(pe->point(0), translatedScenePos);
330 auto *grabber = pe->exclusiveGrabber(pe->point(0));
331
332 if (grabber) {
333 // Temporarily disable the grabber, to stop the delivery agent inside
334 // targetWindow from forwarding the event to an item outside the menu
335 // or menubar. This is especially important to support a press on e.g
336 // a MenuBarItem, followed by a drag-and-release on top of a MenuItem.
337 pe->setExclusiveGrabber(pe->point(0), nullptr);
338 }
339
340 qCDebug(lcPopupWindow) << "forwarding" << pe << "to popup menu:" << targetWindow;
341 QQuickWindowPrivate::get(targetWindow)->deliveryAgent->event(pe);
342
343 // Restore the event before we return
344 QMutableEventPoint::setScenePosition(pe->point(0), scenePos);
345 if (grabber)
346 pe->setExclusiveGrabber(pe->point(0), grabber);
347 } else if (pe->isEndEvent()) {
348 if (!targetPopup && !targetMenuBar) {
349 // Released outside either a popup window, or a menu or menubar that owns a menu using popup windows.
350 // This should normally close the current popup window, unless it's inside the non-client area, which can happen in WASM dialogs.
351 if (event->type() != QEvent::NonClientAreaMouseButtonRelease)
352 closePopups(QQuickPopup::CloseOnReleaseOutside | QQuickPopup::CloseOnReleaseOutsideParent);
353 return false;
354 }
355
356 // To support opening a Menu on press (e.g on a MenuBarItem), followed by
357 // a drag and release on a MenuItem inside the Menu, we ask the Menu to
358 // perform a click on the active MenuItem, if any.
359 if (QQuickMenu *targetMenu = qobject_cast<QQuickMenu *>(targetPopup)) {
360 qCDebug(lcPopupWindow) << "forwarding" << pe << "to popup menu:" << targetMenu;
361 QQuickMenuPrivate::get(targetMenu)->handleReleaseWithoutGrab(pe->point(0));
362 }
363 }
364#endif
365
366 return false;
367}
368
369bool QQuickPopupWindow::event(QEvent *e)
370{
371 Q_D(QQuickPopupWindow);
372 if (e->type() == QEvent::PlatformSurface && static_cast<QPlatformSurfaceEvent *>(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) {
373#if QT_CONFIG(wayland)
374 if (auto *waylandWindow = dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(handle())) {
375 waylandWindow->setExtendedWindowType(QQuickPopupPrivate::get(d->m_popup)->extendedWindowType);
376 waylandWindow->setParentControlGeometry(parentControlGeometry());
377 }
378#endif
379#if QT_CONFIG(xcb)
380 if (auto *xcbWindow = dynamic_cast<QNativeInterface::Private::QXcbWindow *>(handle())) {
381 const auto xcbWindowType = QQuickPopupPrivate::get(d->m_popup)->wmWindowType;
382 if (xcbWindowType != QNativeInterface::Private::QXcbWindow::None)
383 xcbWindow->setWindowType(xcbWindowType);
384 }
385#endif
386 }
387
388 if (d->filterPopupSpecialCases(e))
389 return true;
390
391 if (QQuickPopup *popup = d->m_popup) {
392 // Popups without focus should not consume keyboard events.
393 if (!popup->hasFocus() && (e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease)
394#if QT_CONFIG(shortcut)
395 && (!static_cast<QKeyEvent *>(e)->matches(QKeySequence::Cancel)
396#if defined(Q_OS_ANDROID)
397 || static_cast<QKeyEvent *>(e)->key() != Qt::Key_Back
398#endif
399 )
400#endif
401 ) return false;
402 }
403
404 return QQuickWindowQmlImpl::event(e);
405}
406
407void QQuickPopupWindow::windowChanged(QWindow *window)
408{
409 Q_D(QQuickPopupWindow);
410 if (!d->m_popupParentItemWindow.isNull()) {
411 disconnect(d->m_popupParentItemWindow, &QWindow::xChanged, this, &QQuickPopupWindow::parentWindowXChanged);
412 disconnect(d->m_popupParentItemWindow, &QWindow::yChanged, this, &QQuickPopupWindow::parentWindowYChanged);
413 }
414 if (window) {
415 d->m_popupParentItemWindow = window;
416 connect(window, &QWindow::xChanged, this, &QQuickPopupWindow::parentWindowXChanged);
417 connect(window, &QWindow::yChanged, this, &QQuickPopupWindow::parentWindowYChanged);
418 } else {
419 d->m_popupParentItemWindow.clear();
420 }
421 setTransientParent(window);
422}
423
424std::optional<QPoint> QQuickPopupWindow::global2Local(const QPoint &pos) const
425{
426 Q_D(const QQuickPopupWindow);
427 QQuickPopup *popup = d->m_popup;
428 Q_ASSERT(popup);
429 QWindow *mainWindow = d->m_popupParentItemWindow;
430 if (!mainWindow)
431 mainWindow = transientParent();
432 if (Q_UNLIKELY((!mainWindow || mainWindow != popup->window())))
433 return std::nullopt;
434
435 const QPoint scenePos = mainWindow->mapFromGlobal(pos);
436 // Popup's coordinates are relative to the nearest parent item.
437 return popup->parentItem() ? popup->parentItem()->mapFromScene(scenePos).toPoint() : scenePos;
438}
439
440void QQuickPopupWindow::parentWindowXChanged(int newX)
441{
442 const auto popupLocalPos = global2Local({x(), y()});
443 if (Q_UNLIKELY(!popupLocalPos))
444 return;
445 handlePopupPositionChangeFromWindowSystem({ newX + popupLocalPos->x(), y() });
446}
447
448void QQuickPopupWindow::parentWindowYChanged(int newY)
449{
450 const auto popupLocalPos = global2Local({x(), y()});
451 if (Q_UNLIKELY(!popupLocalPos))
452 return;
453 handlePopupPositionChangeFromWindowSystem({ x(), newY + popupLocalPos->y() });
454}
455
456void QQuickPopupWindow::handlePopupPositionChangeFromWindowSystem(const QPoint &pos)
457{
458 Q_D(QQuickPopupWindow);
459 QQuickPopup *popup = d->m_popup;
460 if (!popup)
461 return;
462
463 const auto windowPos = global2Local(pos);
464 if (Q_LIKELY(windowPos)) {
465 qCDebug(lcPopupWindow).nospace() << "A window system event changed the popup's"
466 << " (" << d->m_popup << ") position to " << *windowPos;
467 QQuickPopupPrivate::get(popup)->setEffectivePosFromWindowPos(*windowPos);
468 }
469}
470
471void QQuickPopupWindow::implicitWidthChanged()
472{
473 Q_D(QQuickPopupWindow);
474 if (auto popup = d->m_popup) {
475 auto *popupPrivate = QQuickPopupPrivate::get(popup);
476 // Include window insets so that it works for styles like FluentWinUI3
477 // that have insets.
478 // Use qCeil to avoid sub-pixel truncation that causes text wrapping
479 // (see QTBUG-130683). This matches the initial sizing logic in
480 // QQuickPopupPrivate::adjustPopupItemParentAndWindow().
481 const QMarginsF insets = popupPrivate->windowInsets();
482 if (!popupPrivate->hasWidth) {
483 setWidth(qCeil(popup->implicitWidth() + insets.left() + insets.right()));
484 d->m_popupItem->setWidth(popup->implicitWidth());
485 }
486 popupPrivate->reposition();
487 }
488}
489
490void QQuickPopupWindow::implicitHeightChanged()
491{
492 Q_D(QQuickPopupWindow);
493 if (auto popup = d->m_popup) {
494 auto *popupPrivate = QQuickPopupPrivate::get(popup);
495 const QMarginsF insets = popupPrivate->windowInsets();
496 if (!popupPrivate->hasHeight) {
497 setHeight(qCeil(popup->implicitHeight() + insets.top() + insets.bottom()));
498 d->m_popupItem->setHeight(popup->implicitHeight());
499 }
500 popupPrivate->reposition();
501 }
502}
503
504#if QT_CONFIG(wayland)
505QRect QQuickPopupWindow::parentControlGeometry() const
506{
507 const QQuickItem *parent = popup()->parentItem();
508 // Menus have an overlap property that we want to honor,
509 // as long as it's not wider than half the width of the parent item.
510 const qreal overlap = popup()->property("overlap").toReal();
511 QRectF parentItemBoundingRect = parent->boundingRect();
512 const QPointF parentItemMappedPosition = parent->mapToScene(parentItemBoundingRect.topLeft());
513 return { qFloor(parentItemMappedPosition.x() + overlap), qFloor(parentItemMappedPosition.y()),
514 qCeil(qMax<qreal>(qAbs(parentItemBoundingRect.width() - overlap * 2), parentItemBoundingRect.width() / 4)), qCeil(parentItemBoundingRect.height()) };
515}
516#endif
517
518QT_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