From d786f9736b29bad3de0b422471bf85c2fa517731 Mon Sep 17 00:00:00 2001 From: Volker Hilsheimer Date: Fri, 17 Sep 2021 15:22:28 +0200 Subject: [PATCH] macOS: send enter/leave when a window opens/closes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since macOS doesn't give us any event when a modal window opens, we need to do so ourselves explicitly so that the current mouse window gets a leave event when e.g. a popup opens, and an enter event when the popup closes again. The case for modal dialogs is partially handled by QGuiApplication already. Note: We cannot rely on the transientParent of the opening/closing window, as it's nullptr for QMenu windows even if the QMenu has a widget parent. Add a test for enter/leave events when a secondary window opens, covering both the dialog and the popup case. For the dialog case, we sometimes get two Enter events when the dailog closes, which we have to tolerate for now. To make the test pass on b2qt platforms, fix the offscreen plugin to explicitly send enter/leave events in the same way as Cocoa now does. Fixes: QTBUG-78970 Change-Id: If45e43e625e8362c3502c740154f6a6a8962b9e9 Reviewed-by: Qt CI Bot Reviewed-by: Tor Arne Vestbø (cherry picked from commit a5e5943d8a7d2a1345dc94dad0a97cf2966f6e7b) Reviewed-by: Qt Cherry-pick Bot --- src/plugins/platforms/cocoa/qcocoawindow.mm | 20 +++ .../platforms/offscreen/qoffscreenwindow.cpp | 16 +++ .../widgets/kernel/qwidget/tst_qwidget.cpp | 127 +++++++++++++++++- 3 files changed, 160 insertions(+), 3 deletions(-) diff --git a/src/plugins/platforms/cocoa/qcocoawindow.mm b/src/plugins/platforms/cocoa/qcocoawindow.mm index 242d442a194..1d5a71865e5 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.mm +++ b/src/plugins/platforms/cocoa/qcocoawindow.mm @@ -1248,12 +1248,32 @@ void QCocoaWindow::windowDidResignKey() void QCocoaWindow::windowDidOrderOnScreen() { + // The current mouse window needs to get a leave event when a popup window opens. + // For modal dialogs, QGuiApplicationPrivate::showModalWindow takes care of this. + if (QWindowPrivate::get(window())->isPopup()) { + QWindowSystemInterface::handleLeaveEvent + (QGuiApplicationPrivate::currentMouseWindow); + } + [m_view setNeedsDisplay:YES]; } void QCocoaWindow::windowDidOrderOffScreen() { handleExposeEvent(QRegion()); + // We are closing a window, so the window that is now under the mouse + // might need to get an Enter event if it isn't already the mouse window. + if (window()->type() & Qt::Window) { + const QPointF screenPoint = QCocoaScreen::mapFromNative([NSEvent mouseLocation]); + if (QWindow *windowUnderMouse = QGuiApplication::topLevelAt(screenPoint.toPoint())) { + if (windowUnderMouse != QGuiApplicationPrivate::instance()->currentMouseWindow) { + const auto windowPoint = windowUnderMouse->mapFromGlobal(screenPoint); + // asynchronous delivery on purpose + QWindowSystemInterface::handleEnterEvent + (windowUnderMouse, windowPoint, screenPoint); + } + } + } } void QCocoaWindow::windowDidChangeOcclusionState() diff --git a/src/plugins/platforms/offscreen/qoffscreenwindow.cpp b/src/plugins/platforms/offscreen/qoffscreenwindow.cpp index a46258a4017..20aae06650d 100644 --- a/src/plugins/platforms/offscreen/qoffscreenwindow.cpp +++ b/src/plugins/platforms/offscreen/qoffscreenwindow.cpp @@ -44,6 +44,7 @@ #include #include +#include QT_BEGIN_NAMESPACE @@ -129,11 +130,26 @@ void QOffscreenWindow::setVisible(bool visible) } } + const QPoint cursorPos = QCursor::pos(); if (visible) { QRect rect(QPoint(), geometry().size()); QWindowSystemInterface::handleExposeEvent(window(), rect); + if (QWindowPrivate::get(window())->isPopup() && QGuiApplicationPrivate::currentMouseWindow) { + QWindowSystemInterface::handleLeaveEvent + (QGuiApplicationPrivate::currentMouseWindow); + } + if (geometry().contains(cursorPos)) + QWindowSystemInterface::handleEnterEvent(window(), + window()->mapFromGlobal(cursorPos), cursorPos); } else { QWindowSystemInterface::handleExposeEvent(window(), QRegion()); + if (window()->type() & Qt::Window) { + if (QWindow *windowUnderMouse = QGuiApplication::topLevelAt(cursorPos)) { + QWindowSystemInterface::handleEnterEvent(windowUnderMouse, + windowUnderMouse->mapFromGlobal(cursorPos), + cursorPos); + } + } } m_visible = visible; diff --git a/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp b/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp index 21e77378d1a..54d502c1090 100644 --- a/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp +++ b/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp @@ -357,6 +357,8 @@ private slots: void maskedUpdate(); #ifndef QT_NO_CURSOR void syntheticEnterLeave(); + void enterLeaveOnWindowShowHide_data(); + void enterLeaveOnWindowShowHide(); void taskQTBUG_4055_sendSyntheticEnterLeave(); void underMouse(); void taskQTBUG_27643_enterEvents(); @@ -9771,6 +9773,124 @@ void tst_QWidget::syntheticEnterLeave() } #endif +#ifndef QT_NO_CURSOR +void tst_QWidget::enterLeaveOnWindowShowHide_data() +{ + QTest::addColumn("windowType"); + QTest::addRow("dialog") << Qt::Dialog; + QTest::addRow("popup") << Qt::Popup; +} + + +/*! + Verify that a window that has the mouse gets a leave event + when a dialog or popup opens (even if that dialog or popup is + not under the mouse), and an enter event when the secondary window + closes again (while the mouse is still over the original widget. + + Since mouse grabbing might cause some event interaction, simulate + the opening of the secondary window from a mouse press, like we would with + a button or context menu. See QTBUG-78970. +*/ +void tst_QWidget::enterLeaveOnWindowShowHide() +{ + QFETCH(Qt::WindowType, windowType); + class Widget : public QWidget + { + public: + int numEnterEvents = 0; + int numLeaveEvents = 0; + QPoint enterPosition; + Qt::WindowType secondaryWindowType = {}; + protected: + void enterEvent(QEnterEvent *e) override + { + enterPosition = e->position().toPoint(); + ++numEnterEvents; + } + void leaveEvent(QEvent *) override + { + enterPosition = {}; + ++numLeaveEvents; + } + void mousePressEvent(QMouseEvent *e) override + { + QWidget *secondary = nullptr; + switch (secondaryWindowType) { + case Qt::Dialog: { + QDialog *dialog = new QDialog(this); + dialog->setModal(true); + dialog->setWindowModality(Qt::ApplicationModal); + secondary = dialog; + break; + } + case Qt::Popup: { + QMenu *menu = new QMenu(this); + menu->addAction("Action 1"); + menu->addAction("Action 2"); + secondary = menu; + break; + } + default: + QVERIFY2(false, "Test case not implemented for window type"); + break; + } + + QPoint secondaryPos = e->globalPosition().toPoint(); + if (e->button() == Qt::LeftButton) + secondaryPos += QPoint(10, 10); // cursor outside secondary + else + secondaryPos -= QPoint(10, 10); // cursor inside secondary + secondary->move(secondaryPos); + secondary->show(); + if (!QTest::qWaitForWindowExposed(secondary)) + QEXPECT_FAIL("", "Secondary window failed to show, test will fail", Abort); + } + }; + + int expectedEnter = 0; + int expectedLeave = 0; + + Widget widget; + widget.secondaryWindowType = windowType; + const QRect screenGeometry = widget.screen()->availableGeometry(); + const QPoint cursorPos = screenGeometry.topLeft() + QPoint(50, 50); + widget.setGeometry(QRect(cursorPos - QPoint(50, 50), screenGeometry.size() / 4)); + QCursor::setPos(cursorPos); + + if (!QTest::qWaitFor([&]{ return widget.geometry().contains(QCursor::pos()); })) + QSKIP("We can't move the cursor"); + widget.show(); + QApplication::setActiveWindow(&widget); + QVERIFY(QTest::qWaitForWindowActive(&widget)); + + ++expectedEnter; + QTRY_COMPARE_WITH_TIMEOUT(widget.numEnterEvents, expectedEnter, 250); + QCOMPARE(widget.enterPosition, widget.mapFromGlobal(cursorPos)); + QVERIFY(widget.underMouse()); + + QTest::mouseClick(&widget, Qt::LeftButton, {}, widget.mapFromGlobal(cursorPos)); + ++expectedLeave; + QTRY_COMPARE_WITH_TIMEOUT(widget.numLeaveEvents, expectedLeave, 500); + QVERIFY(!widget.underMouse()); + if (QApplication::activeModalWidget()) + QApplication::activeModalWidget()->close(); + else if (QApplication::activePopupWidget()) + QApplication::activePopupWidget()->close(); + ++expectedEnter; + // Use default timeout, the test is flaky on Windows otherwise. + QVERIFY(QTest::qWaitFor([&]{ return widget.numEnterEvents >= expectedEnter; })); + // When a modal dialog closes we might get more than one enter event on macOS. + // This seems to depend on timing, so we tolerate that flakiness for now. + if (widget.numEnterEvents > expectedEnter && QGuiApplication::platformName() == "cocoa") + QEXPECT_FAIL("dialog", "On macOS, we might get more than one Enter event", Continue); + + QCOMPARE(widget.numEnterEvents, expectedEnter); + QCOMPARE(widget.enterPosition, widget.mapFromGlobal(cursorPos)); + QVERIFY(widget.underMouse()); +} +#endif + #ifndef QT_NO_CURSOR void tst_QWidget::taskQTBUG_4055_sendSyntheticEnterLeave() { @@ -11214,9 +11334,10 @@ void tst_QWidget::underMouse() QCOMPARE(QApplication::activePopupWidget(), &popupWidget); // Send an artificial leave event for window, as it won't get generated automatically - // due to cursor not actually being over the window. - QWindowSystemInterface::handleLeaveEvent(window); - QApplication::processEvents(); + // due to cursor not actually being over the window. The Cocoa and offscreen plugins + // do this for us. + if (QGuiApplication::platformName() != "cocoa" && QGuiApplication::platformName() != "offscreen") + QWindowSystemInterface::handleLeaveEvent(window); // If there is an active popup, undermouse should not be reported (QTBUG-27478), // but opening a popup causes leave for widgets under mouse.