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 | bool = false; |
36 | |
37 | protected: |
38 | void setVisible(bool visible) override; |
39 | |
40 | private: |
41 | bool filterPopupSpecialCases(QEvent *event); |
42 | }; |
43 | |
44 | QQuickPopupWindow::(QQuickPopup *, QWindow *parent) |
45 | : QQuickWindowQmlImpl(*(new QQuickPopupWindowPrivate), nullptr) |
46 | { |
47 | Q_D(QQuickPopupWindow); |
48 | |
49 | d->m_popup = popup; |
50 | d->m_popupItem = popup->popupItem(); |
51 | setTransientParent(parent); |
52 | |
53 | connect(sender: d->m_popup, signal: &QQuickPopup::windowChanged, context: this, slot: &QQuickPopupWindow::windowChanged); |
54 | connect(sender: d->m_popup, signal: &QQuickPopup::implicitWidthChanged, context: this, slot: &QQuickPopupWindow::implicitWidthChanged); |
55 | connect(sender: d->m_popup, signal: &QQuickPopup::implicitHeightChanged, context: this, slot: &QQuickPopupWindow::implicitHeightChanged); |
56 | connect(sender: d->m_popup->window(), signal: &QWindow::xChanged, context: this, slot: &QQuickPopupWindow::parentWindowXChanged); |
57 | connect(sender: d->m_popup->window(), signal: &QWindow::yChanged, context: this, slot: &QQuickPopupWindow::parentWindowYChanged); |
58 | |
59 | setWidth(d->m_popupItem->implicitWidth()); |
60 | setHeight(d->m_popupItem->implicitHeight()); |
61 | |
62 | const auto flags = QQuickPopupPrivate::get(popup)->popupWindowType(); |
63 | |
64 | // For popup windows, we'll need to draw everything, in order to have enough control over the styling. |
65 | if (flags & Qt::Popup) |
66 | setColor(QColorConstants::Transparent); |
67 | |
68 | setFlags(flags); |
69 | |
70 | qCDebug(lcPopupWindow) << "Created popup window with parent:" << parent << "flags:" << flags; |
71 | } |
72 | |
73 | QQuickPopupWindow::() |
74 | { |
75 | Q_D(QQuickPopupWindow); |
76 | disconnect(sender: d->m_popup, signal: &QQuickPopup::windowChanged, receiver: this, slot: &QQuickPopupWindow::windowChanged); |
77 | disconnect(sender: d->m_popup, signal: &QQuickPopup::implicitWidthChanged, receiver: this, slot: &QQuickPopupWindow::implicitWidthChanged); |
78 | disconnect(sender: d->m_popup, signal: &QQuickPopup::implicitHeightChanged, receiver: this, slot: &QQuickPopupWindow::implicitHeightChanged); |
79 | if (d->m_popup->window()) { |
80 | disconnect(sender: d->m_popup->window(), signal: &QWindow::xChanged, receiver: this, slot: &QQuickPopupWindow::parentWindowXChanged); |
81 | disconnect(sender: d->m_popup->window(), signal: &QWindow::yChanged, receiver: this, slot: &QQuickPopupWindow::parentWindowYChanged); |
82 | } |
83 | } |
84 | |
85 | QQuickPopup *QQuickPopupWindow::() const |
86 | { |
87 | Q_D(const QQuickPopupWindow); |
88 | return d->m_popup; |
89 | } |
90 | |
91 | void QQuickPopupWindow::(QHideEvent *e) |
92 | { |
93 | Q_D(QQuickPopupWindow); |
94 | QQuickWindow::hideEvent(e); |
95 | // Avoid potential infinite recursion, between QWindowPrivate::setVisible(false) and this function. |
96 | QScopedValueRollback<bool>inHideEventRollback(d->m_inHideEvent, true); |
97 | if (QQuickPopup * = d->m_popup) { |
98 | QQuickDialog *dialog = qobject_cast<QQuickDialog *>(object: popup); |
99 | if (dialog && QQuickPopupPrivate::get(popup: dialog)->visible) |
100 | dialog->reject(); |
101 | else |
102 | popup->setVisible(false); |
103 | } |
104 | } |
105 | |
106 | void QQuickPopupWindow::(QMoveEvent *e) |
107 | { |
108 | handlePopupPositionChangeFromWindowSystem(pos: e->pos()); |
109 | } |
110 | |
111 | void QQuickPopupWindow::(QResizeEvent *e) |
112 | { |
113 | Q_D(QQuickPopupWindow); |
114 | QQuickWindowQmlImpl::resizeEvent(e); |
115 | |
116 | if (!d->m_popupItem) |
117 | return; |
118 | |
119 | qCDebug(lcPopupWindow) << "A window system event changed the popup's size to be " << e->size(); |
120 | QQuickPopupPrivate * = QQuickPopupPrivate::get(popup: d->m_popup); |
121 | |
122 | const auto topLeftFromSystem = global2Local(pos: 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 | popupPrivate->x = topLeftFromSystem.x(); |
128 | popupPrivate->y = topLeftFromSystem.y(); |
129 | |
130 | const QMarginsF windowInsets = popupPrivate->windowInsets(); |
131 | d->m_popupItem->setWidth(e->size().width() - windowInsets.left() - windowInsets.right()); |
132 | d->m_popupItem->setHeight(e->size().height() - windowInsets.top() - windowInsets.bottom()); |
133 | |
134 | // and restore the actual x and y afterwards |
135 | popupPrivate->x = oldX; |
136 | popupPrivate->y = oldY; |
137 | } |
138 | |
139 | void QQuickPopupWindowPrivate::(bool visible) |
140 | { |
141 | if (m_inHideEvent) |
142 | return; |
143 | |
144 | const bool visibleChanged = QWindowPrivate::visible != visible; |
145 | |
146 | // Check if we're about to close the last popup, in which case, ungrab. |
147 | if (!visible && visibleChanged && QGuiApplicationPrivate::popupCount() == 1 && s_grabbedWindow) { |
148 | s_grabbedWindow->setMouseGrabEnabled(false); |
149 | s_grabbedWindow->setKeyboardGrabEnabled(false); |
150 | s_popupGrabOk = false; |
151 | qCDebug(lcPopupWindow) << "The window " << s_grabbedWindow << "has disabled global mouse and keyboard grabs." ; |
152 | s_grabbedWindow = nullptr; |
153 | } |
154 | |
155 | QQuickWindowQmlImplPrivate::setVisible(visible); |
156 | |
157 | // Similar logic to grabForPopup(QWidget *popup) |
158 | // If the user clicks outside, popups with CloseOnPressOutside*/CloseOnReleaseOutside* need to be able to react, |
159 | // in order to determine if they should close. |
160 | // 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. |
161 | if (visible && visibleChanged && QGuiApplicationPrivate::popupCount() == 1 && !s_popupGrabOk) { |
162 | QWindow *win = m_popup->window(); |
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 | */ |
196 | bool QQuickPopupWindowPrivate::(QEvent *event) |
197 | { |
198 | Q_Q(QQuickPopupWindow); |
199 | |
200 | if (!event->isPointerEvent()) |
201 | return false; |
202 | |
203 | QQuickPopup * = 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 * = QQuickPopupPrivate::get(popup)->contains(scenePos: contentItem->mapFromGlobal(point: globalPos)) ? popup : nullptr; |
211 | |
212 | // Resolve the Menu or MenuBar under the mouse, if any |
213 | QQuickMenu * = qobject_cast<QQuickMenu *>(object: popup); |
214 | QQuickMenuBar * = nullptr; |
215 | QObject * = menu; |
216 | while (menuParent) { |
217 | if (auto * = qobject_cast<QQuickMenu *>(object: menuParent)) { |
218 | QQuickPopupWindow * = QQuickMenuPrivate::get(menu: parentMenu)->popupWindow; |
219 | auto * = QQuickPopupPrivate::get(popup: popupWindow->popup()); |
220 | QPointF scenePos = popupWindow->contentItem()->mapFromGlobal(point: globalPos); |
221 | if (popup_d->contains(scenePos)) { |
222 | targetPopup = parentMenu; |
223 | break; |
224 | } |
225 | } else if (auto * = qobject_cast<QQuickMenuBar *>(object: menuParent)) { |
226 | const QPointF = menuBar->mapFromGlobal(point: globalPos); |
227 | if (menuBar->contains(point: 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 *>(object: 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(flags: 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(popup: 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(i: 0).scenePosition(); |
272 | const auto translatedScenePos = targetWindow->mapFromGlobal(pos: globalPos); |
273 | QMutableEventPoint::setScenePosition(p&: pe->point(i: 0), arg: translatedScenePos); |
274 | auto *grabber = pe->exclusiveGrabber(point: pe->point(i: 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(point: pe->point(i: 0), exclusiveGrabber: nullptr); |
282 | } |
283 | |
284 | qCDebug(lcPopupWindow) << "forwarding" << pe << "to popup menu:" << targetWindow; |
285 | QQuickWindowPrivate::get(c: targetWindow)->deliveryAgent->event(ev: pe); |
286 | |
287 | // Restore the event before we return |
288 | QMutableEventPoint::setScenePosition(p&: pe->point(i: 0), arg: scenePos); |
289 | if (grabber) |
290 | pe->setExclusiveGrabber(point: pe->point(i: 0), exclusiveGrabber: grabber); |
291 | } else if (pe->isEndEvent()) { |
292 | if (!targetPopup && !targetMenuBar && closePolicy.testAnyFlags(flags: 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 * = qobject_cast<QQuickMenu *>(object: targetPopup)) |
302 | QQuickMenuPrivate::get(menu: targetMenu)->handleReleaseWithoutGrab(eventPoint: pe->point(i: 0)); |
303 | } |
304 | |
305 | return false; |
306 | } |
307 | |
308 | bool QQuickPopupWindow::(QEvent *e) |
309 | { |
310 | Q_D(QQuickPopupWindow); |
311 | if (d->filterPopupSpecialCases(event: e)) |
312 | return true; |
313 | |
314 | if (QQuickPopup * = d->m_popup) { |
315 | // Popups without focus should not consume keyboard events. |
316 | if (!popup->hasFocus() && (e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease) |
317 | #if QT_CONFIG(shortcut) |
318 | && (!static_cast<QKeyEvent *>(e)->matches(key: QKeySequence::Cancel) |
319 | #if defined(Q_OS_ANDROID) |
320 | || static_cast<QKeyEvent *>(e)->key() != Qt::Key_Back |
321 | #endif |
322 | ) |
323 | #endif |
324 | ) return false; |
325 | } |
326 | |
327 | return QQuickWindowQmlImpl::event(e); |
328 | } |
329 | |
330 | void QQuickPopupWindow::(QWindow *window) |
331 | { |
332 | if (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 | } |
336 | } |
337 | |
338 | QPoint QQuickPopupWindow::(const QPoint &pos) const |
339 | { |
340 | Q_D(const QQuickPopupWindow); |
341 | QQuickPopup * = d->m_popup; |
342 | Q_ASSERT(popup); |
343 | const QPoint scenePos = popup->window()->mapFromGlobal(pos); |
344 | // Popup's coordinates are relative to the nearest parent item. |
345 | return popup->parentItem() ? popup->parentItem()->mapFromScene(point: scenePos).toPoint() : scenePos; |
346 | } |
347 | |
348 | void QQuickPopupWindow::(int newX) |
349 | { |
350 | const auto = global2Local(pos: {x(), y()}); |
351 | handlePopupPositionChangeFromWindowSystem(pos: {newX + popupLocalPos.x(), y()}); |
352 | } |
353 | |
354 | void QQuickPopupWindow::(int newY) |
355 | { |
356 | const auto = global2Local(pos: {x(), y()}); |
357 | handlePopupPositionChangeFromWindowSystem(pos: {x(), newY + popupLocalPos.y()}); |
358 | } |
359 | |
360 | void QQuickPopupWindow::handlePopupPositionChangeFromWindowSystem(const QPoint &pos) |
361 | { |
362 | Q_D(QQuickPopupWindow); |
363 | QQuickPopup * = d->m_popup; |
364 | if (!popup || !popup->window()) |
365 | return; |
366 | QQuickPopupPrivate * = QQuickPopupPrivate::get(popup); |
367 | |
368 | const auto windowPos = global2Local(pos); |
369 | qCDebug(lcPopupWindow) << "A window system event changed the popup's position to be " << windowPos; |
370 | popupPrivate->setEffectivePosFromWindowPos(windowPos); |
371 | } |
372 | |
373 | void QQuickPopupWindow::() |
374 | { |
375 | Q_D(const QQuickPopupWindow); |
376 | if (auto = d->m_popup) |
377 | setWidth(popup->implicitWidth()); |
378 | } |
379 | |
380 | void QQuickPopupWindow::() |
381 | { |
382 | Q_D(const QQuickPopupWindow); |
383 | if (auto = d->m_popup) |
384 | setHeight(popup->implicitHeight()); |
385 | } |
386 | |
387 | QT_END_NAMESPACE |
388 | |
389 | |