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