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