| 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 | |