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 // Forward move events to the target window
298 const auto scenePos = pe->point(0).scenePosition();
299 const auto translatedScenePos = targetWindow->mapFromGlobal(globalPos);
300 QMutableEventPoint::setScenePosition(pe->point(0), translatedScenePos);
301 auto *grabber = pe->exclusiveGrabber(pe->point(0));
302
303 if (grabber) {
304 // Temporarily disable the grabber, to stop the delivery agent inside
305 // targetWindow from forwarding the event to an item outside the menu
306 // or menubar. This is especially important to support a press on e.g
307 // a MenuBarItem, followed by a drag-and-release on top of a MenuItem.
308 pe->setExclusiveGrabber(pe->point(0), nullptr);
309 }
310
311 qCDebug(lcPopupWindow) << "forwarding" << pe << "to popup menu:" << targetWindow;
312 QQuickWindowPrivate::get(targetWindow)->deliveryAgent->event(pe);
313
314 // Restore the event before we return
315 QMutableEventPoint::setScenePosition(pe->point(0), scenePos);
316 if (grabber)
317 pe->setExclusiveGrabber(pe->point(0), grabber);
318 } else if (pe->isEndEvent()) {
319 if (!targetPopup && !targetMenuBar && closePolicy.testAnyFlags(QQuickPopup::CloseOnReleaseOutside | QQuickPopup::CloseOnReleaseOutsideParent)) {
320 // Released outside either a popup window, or a menu or menubar that owns a menu using popup windows.
321 // This should normally close the current popup window, unless it's inside the non-client area, which can happen in WASM dialogs.
322 if (event->type() != QEvent::NonClientAreaMouseButtonRelease)
323 closePopupAndParentMenus();
324 return false;
325 }
326
327 // To support opening a Menu on press (e.g on a MenuBarItem), followed by
328 // a drag and release on a MenuItem inside the Menu, we ask the Menu to
329 // perform a click on the active MenuItem, if any.
330 if (QQuickMenu *targetMenu = qobject_cast<QQuickMenu *>(targetPopup)) {
331 qCDebug(lcPopupWindow) << "forwarding" << pe << "to popup menu:" << targetMenu;
332 QQuickMenuPrivate::get(targetMenu)->handleReleaseWithoutGrab(pe->point(0));
333 }
334 }
335#endif
336
337 return false;
338}
339
340bool QQuickPopupWindow::event(QEvent *e)
341{
342 Q_D(QQuickPopupWindow);
343#if QT_CONFIG(wayland)
344 if (e->type() == QEvent::PlatformSurface && static_cast<QPlatformSurfaceEvent *>(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) {
345 if (auto *waylandWindow = dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(handle())) {
346 waylandWindow->setExtendedWindowType(QQuickPopupPrivate::get(d->m_popup)->extendedWindowType);
347 waylandWindow->setParentControlGeometry(parentControlGeometry());
348 }
349 }
350#endif
351
352 if (d->filterPopupSpecialCases(e))
353 return true;
354
355 if (QQuickPopup *popup = d->m_popup) {
356 // Popups without focus should not consume keyboard events.
357 if (!popup->hasFocus() && (e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease)
358#if QT_CONFIG(shortcut)
359 && (!static_cast<QKeyEvent *>(e)->matches(QKeySequence::Cancel)
360#if defined(Q_OS_ANDROID)
361 || static_cast<QKeyEvent *>(e)->key() != Qt::Key_Back
362#endif
363 )
364#endif
365 ) return false;
366 }
367
368 return QQuickWindowQmlImpl::event(e);
369}
370
371void QQuickPopupWindow::windowChanged(QWindow *window)
372{
373 Q_D(QQuickPopupWindow);
374 if (!d->m_popupParentItemWindow.isNull()) {
375 disconnect(d->m_popupParentItemWindow, &QWindow::xChanged, this, &QQuickPopupWindow::parentWindowXChanged);
376 disconnect(d->m_popupParentItemWindow, &QWindow::yChanged, this, &QQuickPopupWindow::parentWindowYChanged);
377 }
378 if (window) {
379 d->m_popupParentItemWindow = window;
380 connect(window, &QWindow::xChanged, this, &QQuickPopupWindow::parentWindowXChanged);
381 connect(window, &QWindow::yChanged, this, &QQuickPopupWindow::parentWindowYChanged);
382 } else {
383 d->m_popupParentItemWindow.clear();
384 }
385}
386
387std::optional<QPoint> QQuickPopupWindow::global2Local(const QPoint &pos) const
388{
389 Q_D(const QQuickPopupWindow);
390 QQuickPopup *popup = d->m_popup;
391 Q_ASSERT(popup);
392 QWindow *mainWindow = d->m_popupParentItemWindow;
393 if (!mainWindow)
394 mainWindow = transientParent();
395 if (Q_UNLIKELY((!mainWindow || mainWindow != popup->window())))
396 return std::nullopt;
397
398 const QPoint scenePos = mainWindow->mapFromGlobal(pos);
399 // Popup's coordinates are relative to the nearest parent item.
400 return popup->parentItem() ? popup->parentItem()->mapFromScene(scenePos).toPoint() : scenePos;
401}
402
403void QQuickPopupWindow::parentWindowXChanged(int newX)
404{
405 const auto popupLocalPos = global2Local({x(), y()});
406 if (Q_UNLIKELY(!popupLocalPos))
407 return;
408 handlePopupPositionChangeFromWindowSystem({ newX + popupLocalPos->x(), y() });
409}
410
411void QQuickPopupWindow::parentWindowYChanged(int newY)
412{
413 const auto popupLocalPos = global2Local({x(), y()});
414 if (Q_UNLIKELY(!popupLocalPos))
415 return;
416 handlePopupPositionChangeFromWindowSystem({ x(), newY + popupLocalPos->y() });
417}
418
419void QQuickPopupWindow::handlePopupPositionChangeFromWindowSystem(const QPoint &pos)
420{
421 Q_D(QQuickPopupWindow);
422 QQuickPopup *popup = d->m_popup;
423 if (!popup)
424 return;
425
426 const auto windowPos = global2Local(pos);
427 if (Q_LIKELY(windowPos)) {
428 qCDebug(lcPopupWindow).nospace() << "A window system event changed the popup's"
429 << " (" << d->m_popup << ") position to " << *windowPos;
430 QQuickPopupPrivate::get(popup)->setEffectivePosFromWindowPos(*windowPos);
431 }
432}
433
434void QQuickPopupWindow::implicitWidthChanged()
435{
436 Q_D(QQuickPopupWindow);
437 if (auto popup = d->m_popup) {
438 auto *popupPrivate = QQuickPopupPrivate::get(popup);
439 // Include window insets so that it works for styles like FluentWinUI3
440 // that have insets.
441 // Use qCeil to avoid sub-pixel truncation that causes text wrapping
442 // (see QTBUG-130683). This matches the initial sizing logic in
443 // QQuickPopupPrivate::adjustPopupItemParentAndWindow().
444 const QMarginsF insets = popupPrivate->windowInsets();
445 setWidth(qCeil(popup->implicitWidth() + insets.left() + insets.right()));
446 d->m_popupItem->setWidth(popup->implicitWidth());
447 popupPrivate->reposition();
448 }
449}
450
451void QQuickPopupWindow::implicitHeightChanged()
452{
453 Q_D(QQuickPopupWindow);
454 if (auto popup = d->m_popup) {
455 auto *popupPrivate = QQuickPopupPrivate::get(popup);
456 const QMarginsF insets = popupPrivate->windowInsets();
457 setHeight(qCeil(popup->implicitHeight() + insets.top() + insets.bottom()));
458 d->m_popupItem->setHeight(popup->implicitHeight());
459 popupPrivate->reposition();
460 }
461}
462
463#if QT_CONFIG(wayland)
464QRect QQuickPopupWindow::parentControlGeometry() const
465{
466 const QQuickItem *parent = popup()->parentItem();
467 // Menus have an overlap property that we want to honor,
468 // as long as it's not wider than half the width of the parent item.
469 const qreal overlap = popup()->property("overlap").toReal();
470 QRectF parentItemBoundingRect = parent->boundingRect();
471 const QPointF parentItemMappedPosition = parent->mapToScene(parentItemBoundingRect.topLeft());
472 return { qFloor(parentItemMappedPosition.x() + overlap), qFloor(parentItemMappedPosition.y()),
473 qCeil(qMax<qreal>(qAbs(parentItemBoundingRect.width() - overlap * 2), parentItemBoundingRect.width() / 4)), qCeil(parentItemBoundingRect.height()) };
474}
475#endif
476
477QT_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