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