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 const QQuickPopup::ClosePolicy closePolicy = popup->closePolicy();
226 QQuickPopup *targetPopup = QQuickPopupPrivate::get(popup)->contains(contentItem->mapFromGlobal(globalPos)) ? popup : nullptr;
227
228 // Resolve the Menu or MenuBar under the mouse, if any
229 QQuickMenu *menu = qobject_cast<QQuickMenu *>(popup);
230 QQuickMenuBar *targetMenuBar = nullptr;
231 QObject *menuParent = menu;
232 while (menuParent) {
233 if (auto *parentMenu = qobject_cast<QQuickMenu *>(menuParent)) {
234 QQuickPopupWindow *popupWindow = QQuickMenuPrivate::get(parentMenu)->popupWindow;
235 auto *popup_d = QQuickPopupPrivate::get(popupWindow->popup());
236 QPointF scenePos = popupWindow->contentItem()->mapFromGlobal(globalPos);
237 if (popup_d->contains(scenePos)) {
238 targetPopup = parentMenu;
239 break;
240 }
241 } else if (auto *menuBar = qobject_cast<QQuickMenuBar *>(menuParent)) {
242 const QPointF menuBarPos = menuBar->mapFromGlobal(globalPos);
243 if (menuBar->contains(menuBarPos))
244 targetMenuBar = menuBar;
245 break;
246 }
247
248 menuParent = menuParent->parent();
249 }
250
251 auto closePopupAndParentMenus = [q]() {
252 QQuickPopup *current = q->popup();
253 do {
254 qCDebug(lcPopupWindow) << "Closing" << current << "from an outside pointer press or release event";
255 current->close();
256 current = qobject_cast<QQuickMenu *>(current->parent());
257 } while (current);
258 };
259
260 if (pe->isBeginEvent()) {
261 if (targetMenuBar) {
262 // If the press was on top of the menu bar, we close all menus and return
263 // true. The latter will stop QGuiApplication from propagating the event
264 // to the window under the pointer, and therefore also to the MenuBar.
265 // The latter would otherwise cause a menu to reopen again immediately, and
266 // undermine that we want to close all popups.
267 closePopupAndParentMenus();
268 return true;
269 } else if (!targetPopup) {
270 // Pressed outside either a popup window, or a menu or menubar that owns a menu using popup windows.
271 // Note that A QQuickPopupWindow can be bigger than the
272 // menu itself, to make room for a drop-shadow. But if the press was on top
273 // of the shadow, targetMenu will still be nullptr.
274 // 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.
275 if (event->type() != QEvent::NonClientAreaMouseButtonPress && event->type() != QEvent::NonClientAreaMouseButtonDblClick) {
276 if (closePolicy.testAnyFlags(QQuickPopup::CloseOnPressOutside | QQuickPopup::CloseOnPressOutsideParent))
277 closePopupAndParentMenus();
278 // A modal popup must consume the press event so that forwardToPopup() sees the event as handled
279 // so that it doesn't propagate to items behind the popup window (QTBUG-131786 etc.)
280 // A QTabletEvent in particular is not accepted by default
281 if (popup->isModal()) {
282 pe->accept();
283 return true;
284 }
285 }
286 return false;
287 }
288 } else if (pe->isUpdateEvent()){
289 QQuickWindow *targetWindow = nullptr;
290 if (targetPopup)
291 targetWindow = QQuickPopupPrivate::get(targetPopup)->popupWindow;
292 else if (targetMenuBar)
293 targetWindow = targetMenuBar->window();
294 else
295 return false;
296
297 // If the target is this popup window itself, normal event delivery will
298 // handle it (e.g. an exclusive grabber like a Slider). Forwarding to the
299 // same DA with the grabber cleared would break drag operations.
300 if (targetWindow == q)
301 return false;
302
303 // Forward move events to the target window
304 const auto scenePos = pe->point(0).scenePosition();
305 const auto translatedScenePos = targetWindow->mapFromGlobal(globalPos);
306 QMutableEventPoint::setScenePosition(pe->point(0), translatedScenePos);
307 auto *grabber = pe->exclusiveGrabber(pe->point(0));
308
309 if (grabber) {
310 // Temporarily disable the grabber, to stop the delivery agent inside
311 // targetWindow from forwarding the event to an item outside the menu
312 // or menubar. This is especially important to support a press on e.g
313 // a MenuBarItem, followed by a drag-and-release on top of a MenuItem.
314 pe->setExclusiveGrabber(pe->point(0), nullptr);
315 }
316
317 qCDebug(lcPopupWindow) << "forwarding" << pe << "to popup menu:" << targetWindow;
318 QQuickWindowPrivate::get(targetWindow)->deliveryAgent->event(pe);
319
320 // Restore the event before we return
321 QMutableEventPoint::setScenePosition(pe->point(0), scenePos);
322 if (grabber)
323 pe->setExclusiveGrabber(pe->point(0), grabber);
324 } else if (pe->isEndEvent()) {
325 if (!targetPopup && !targetMenuBar && closePolicy.testAnyFlags(QQuickPopup::CloseOnReleaseOutside | QQuickPopup::CloseOnReleaseOutsideParent)) {
326 // Released outside either a popup window, or a menu or menubar that owns a menu using popup windows.
327 // This should normally close the current popup window, unless it's inside the non-client area, which can happen in WASM dialogs.
328 if (event->type() != QEvent::NonClientAreaMouseButtonRelease)
329 closePopupAndParentMenus();
330 return false;
331 }
332
333 // To support opening a Menu on press (e.g on a MenuBarItem), followed by
334 // a drag and release on a MenuItem inside the Menu, we ask the Menu to
335 // perform a click on the active MenuItem, if any.
336 if (QQuickMenu *targetMenu = qobject_cast<QQuickMenu *>(targetPopup)) {
337 qCDebug(lcPopupWindow) << "forwarding" << pe << "to popup menu:" << targetMenu;
338 QQuickMenuPrivate::get(targetMenu)->handleReleaseWithoutGrab(pe->point(0));
339 }
340 }
341#endif
342
343 return false;
344}
345
346bool QQuickPopupWindow::event(QEvent *e)
347{
348 Q_D(QQuickPopupWindow);
349 if (e->type() == QEvent::PlatformSurface && static_cast<QPlatformSurfaceEvent *>(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) {
350#if QT_CONFIG(wayland)
351 if (auto *waylandWindow = dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(handle())) {
352 waylandWindow->setExtendedWindowType(QQuickPopupPrivate::get(d->m_popup)->extendedWindowType);
353 waylandWindow->setParentControlGeometry(parentControlGeometry());
354 }
355#endif
356#if QT_CONFIG(xcb)
357 if (auto *xcbWindow = dynamic_cast<QNativeInterface::Private::QXcbWindow *>(handle())) {
358 const auto xcbWindowType = QQuickPopupPrivate::get(d->m_popup)->wmWindowType;
359 if (xcbWindowType != QNativeInterface::Private::QXcbWindow::None)
360 xcbWindow->setWindowType(xcbWindowType);
361 }
362#endif
363 }
364
365 if (d->filterPopupSpecialCases(e))
366 return true;
367
368 if (QQuickPopup *popup = d->m_popup) {
369 // Popups without focus should not consume keyboard events.
370 if (!popup->hasFocus() && (e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease)
371#if QT_CONFIG(shortcut)
372 && (!static_cast<QKeyEvent *>(e)->matches(QKeySequence::Cancel)
373#if defined(Q_OS_ANDROID)
374 || static_cast<QKeyEvent *>(e)->key() != Qt::Key_Back
375#endif
376 )
377#endif
378 ) return false;
379 }
380
381 return QQuickWindowQmlImpl::event(e);
382}
383
384void QQuickPopupWindow::windowChanged(QWindow *window)
385{
386 Q_D(QQuickPopupWindow);
387 if (!d->m_popupParentItemWindow.isNull()) {
388 disconnect(d->m_popupParentItemWindow, &QWindow::xChanged, this, &QQuickPopupWindow::parentWindowXChanged);
389 disconnect(d->m_popupParentItemWindow, &QWindow::yChanged, this, &QQuickPopupWindow::parentWindowYChanged);
390 }
391 if (window) {
392 d->m_popupParentItemWindow = window;
393 connect(window, &QWindow::xChanged, this, &QQuickPopupWindow::parentWindowXChanged);
394 connect(window, &QWindow::yChanged, this, &QQuickPopupWindow::parentWindowYChanged);
395 } else {
396 d->m_popupParentItemWindow.clear();
397 }
398 setTransientParent(window);
399}
400
401std::optional<QPoint> QQuickPopupWindow::global2Local(const QPoint &pos) const
402{
403 Q_D(const QQuickPopupWindow);
404 QQuickPopup *popup = d->m_popup;
405 Q_ASSERT(popup);
406 QWindow *mainWindow = d->m_popupParentItemWindow;
407 if (!mainWindow)
408 mainWindow = transientParent();
409 if (Q_UNLIKELY((!mainWindow || mainWindow != popup->window())))
410 return std::nullopt;
411
412 const QPoint scenePos = mainWindow->mapFromGlobal(pos);
413 // Popup's coordinates are relative to the nearest parent item.
414 return popup->parentItem() ? popup->parentItem()->mapFromScene(scenePos).toPoint() : scenePos;
415}
416
417void QQuickPopupWindow::parentWindowXChanged(int newX)
418{
419 const auto popupLocalPos = global2Local({x(), y()});
420 if (Q_UNLIKELY(!popupLocalPos))
421 return;
422 handlePopupPositionChangeFromWindowSystem({ newX + popupLocalPos->x(), y() });
423}
424
425void QQuickPopupWindow::parentWindowYChanged(int newY)
426{
427 const auto popupLocalPos = global2Local({x(), y()});
428 if (Q_UNLIKELY(!popupLocalPos))
429 return;
430 handlePopupPositionChangeFromWindowSystem({ x(), newY + popupLocalPos->y() });
431}
432
433void QQuickPopupWindow::handlePopupPositionChangeFromWindowSystem(const QPoint &pos)
434{
435 Q_D(QQuickPopupWindow);
436 QQuickPopup *popup = d->m_popup;
437 if (!popup)
438 return;
439
440 const auto windowPos = global2Local(pos);
441 if (Q_LIKELY(windowPos)) {
442 qCDebug(lcPopupWindow).nospace() << "A window system event changed the popup's"
443 << " (" << d->m_popup << ") position to " << *windowPos;
444 QQuickPopupPrivate::get(popup)->setEffectivePosFromWindowPos(*windowPos);
445 }
446}
447
448void QQuickPopupWindow::implicitWidthChanged()
449{
450 Q_D(QQuickPopupWindow);
451 if (auto popup = d->m_popup) {
452 auto *popupPrivate = QQuickPopupPrivate::get(popup);
453 // Include window insets so that it works for styles like FluentWinUI3
454 // that have insets.
455 // Use qCeil to avoid sub-pixel truncation that causes text wrapping
456 // (see QTBUG-130683). This matches the initial sizing logic in
457 // QQuickPopupPrivate::adjustPopupItemParentAndWindow().
458 const QMarginsF insets = popupPrivate->windowInsets();
459 setWidth(qCeil(popup->implicitWidth() + insets.left() + insets.right()));
460 d->m_popupItem->setWidth(popup->implicitWidth());
461 popupPrivate->reposition();
462 }
463}
464
465void QQuickPopupWindow::implicitHeightChanged()
466{
467 Q_D(QQuickPopupWindow);
468 if (auto popup = d->m_popup) {
469 auto *popupPrivate = QQuickPopupPrivate::get(popup);
470 const QMarginsF insets = popupPrivate->windowInsets();
471 setHeight(qCeil(popup->implicitHeight() + insets.top() + insets.bottom()));
472 d->m_popupItem->setHeight(popup->implicitHeight());
473 popupPrivate->reposition();
474 }
475}
476
477#if QT_CONFIG(wayland)
478QRect QQuickPopupWindow::parentControlGeometry() const
479{
480 const QQuickItem *parent = popup()->parentItem();
481 // Menus have an overlap property that we want to honor,
482 // as long as it's not wider than half the width of the parent item.
483 const qreal overlap = popup()->property("overlap").toReal();
484 QRectF parentItemBoundingRect = parent->boundingRect();
485 const QPointF parentItemMappedPosition = parent->mapToScene(parentItemBoundingRect.topLeft());
486 return { qFloor(parentItemMappedPosition.x() + overlap), qFloor(parentItemMappedPosition.y()),
487 qCeil(qMax<qreal>(qAbs(parentItemBoundingRect.width() - overlap * 2), parentItemBoundingRect.width() / 4)), qCeil(parentItemBoundingRect.height()) };
488}
489#endif
490
491QT_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