macOS: send enter/leave when a window opens/closes

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
Pick-to: 6.2
Change-Id: If45e43e625e8362c3502c740154f6a6a8962b9e9
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
This commit is contained in:
Volker Hilsheimer 2021-09-17 15:22:28 +02:00
parent 8da42e1af6
commit a5e5943d8a
3 changed files with 160 additions and 3 deletions

View File

@ -1225,12 +1225,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<QWindowSystemInterface::SynchronousDelivery>
(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<QWindowSystemInterface::AsynchronousDelivery>
(windowUnderMouse, windowPoint, screenPoint);
}
}
}
}
void QCocoaWindow::windowDidChangeOcclusionState()

View File

@ -44,6 +44,7 @@
#include <qpa/qwindowsysteminterface.h>
#include <private/qwindow_p.h>
#include <private/qguiapplication_p.h>
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<QWindowSystemInterface::SynchronousDelivery>
(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;

View File

@ -358,6 +358,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();
@ -9838,6 +9840,124 @@ void tst_QWidget::syntheticEnterLeave()
}
#endif
#ifndef QT_NO_CURSOR
void tst_QWidget::enterLeaveOnWindowShowHide_data()
{
QTest::addColumn<Qt::WindowType>("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()
{
@ -11281,9 +11401,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<QWindowSystemInterface::SynchronousDelivery>(window);
// If there is an active popup, undermouse should not be reported (QTBUG-27478),
// but opening a popup causes leave for widgets under mouse.