From 57ecd5aeeb1f609206933be66b92fcdf703703d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?B=C5=82a=C5=BCej=20Szczygie=C5=82?= <spaz16@wp.pl>
Date: Thu, 21 Jan 2016 13:19:37 +0100
Subject: [PATCH] QtWidgets: Proper delivery of enter/leave event to context
 menus

First-level context menu grabs the mouse, so all mouse events are
delivered to it. This menu passes the mouse events to submenus. Any
platform delivers mouse enter/leave event differently when window is
grabbed. This patch unifies event delivery to context menus - it can
block some unwanted events and it emulates fake events if necessary.

This patch can reduce duplicated events and can provide proper enter
or leave event to additional widgets in the context menu. It can also
prevent submenu from unwanted close on Windows and X11.

Added autotest.

Task-number: QTBUG-45565
Task-number: QTBUG-45893
Task-number: QTBUG-47515
Change-Id: I7dd476d0be23afa34e947e54aef235012d173dcf
Reviewed-by: Shawn Rutledge <shawn.rutledge@theqtcompany.com>
---
 src/widgets/kernel/qwidgetwindow.cpp          |  54 +++++++--
 .../auto/widgets/widgets/qmenu/tst_qmenu.cpp  | 109 ++++++++++++++++++
 2 files changed, 151 insertions(+), 12 deletions(-)

diff --git a/src/widgets/kernel/qwidgetwindow.cpp b/src/widgets/kernel/qwidgetwindow.cpp
index 0de0fe21f26..b2a2ecc3b95 100644
--- a/src/widgets/kernel/qwidgetwindow.cpp
+++ b/src/widgets/kernel/qwidgetwindow.cpp
@@ -313,6 +313,14 @@ QPointer<QWidget> qt_last_mouse_receiver = 0;
 
 void QWidgetWindow::handleEnterLeaveEvent(QEvent *event)
 {
+#if !defined(Q_OS_OSX) && !defined(Q_OS_IOS) // Cocoa tracks popups
+    // Ignore all enter/leave events from QPA if we are not on the first-level context menu.
+    // This prevents duplicated events on most platforms. Fake events will be delivered in
+    // QWidgetWindow::handleMouseEvent(QMouseEvent *). Make an exception whether the widget
+    // is already under mouse - let the mouse leave.
+    if (QApplicationPrivate::inPopupMode() && m_widget != QApplication::activePopupWidget() && !m_widget->underMouse())
+        return;
+#endif
     if (event->type() == QEvent::Leave) {
         QWidget *enter = 0;
         // Check from window system event queue if the next queued enter targets a window
@@ -407,14 +415,13 @@ void QWidgetWindow::handleMouseEvent(QMouseEvent *event)
         QEvent::MouseButtonRelease : QEvent::MouseButtonPress;
     if (qApp->d_func()->inPopupMode()) {
         QWidget *activePopupWidget = qApp->activePopupWidget();
-        QWidget *popup = activePopupWidget;
         QPoint mapped = event->pos();
-        if (popup != m_widget)
-            mapped = popup->mapFromGlobal(event->globalPos());
+        if (activePopupWidget != m_widget)
+            mapped = activePopupWidget->mapFromGlobal(event->globalPos());
         bool releaseAfter = false;
-        QWidget *popupChild  = popup->childAt(mapped);
+        QWidget *popupChild  = activePopupWidget->childAt(mapped);
 
-        if (popup != qt_popup_down) {
+        if (activePopupWidget != qt_popup_down) {
             qt_button_down = 0;
             qt_popup_down = 0;
         }
@@ -423,7 +430,7 @@ void QWidgetWindow::handleMouseEvent(QMouseEvent *event)
         case QEvent::MouseButtonPress:
         case QEvent::MouseButtonDblClick:
             qt_button_down = popupChild;
-            qt_popup_down = popup;
+            qt_popup_down = activePopupWidget;
             break;
         case QEvent::MouseButtonRelease:
             releaseAfter = true;
@@ -434,18 +441,41 @@ void QWidgetWindow::handleMouseEvent(QMouseEvent *event)
 
         int oldOpenPopupCount = openPopupCount;
 
-        if (popup->isEnabled()) {
+        if (activePopupWidget->isEnabled()) {
             // deliver event
             qt_replay_popup_mouse_event = false;
-            QWidget *receiver = popup;
+            QWidget *receiver = activePopupWidget;
             QPoint widgetPos = mapped;
             if (qt_button_down)
                 receiver = qt_button_down;
             else if (popupChild)
                 receiver = popupChild;
-            if (receiver != popup)
+            if (receiver != activePopupWidget)
                 widgetPos = receiver->mapFromGlobal(event->globalPos());
-            QWidget *alien = m_widget->childAt(m_widget->mapFromGlobal(event->globalPos()));
+            QWidget *alien = receiver;
+
+#if !defined(Q_OS_OSX) && !defined(Q_OS_IOS) // Cocoa tracks popups
+            const bool reallyUnderMouse = activePopupWidget->rect().contains(mapped);
+            const bool underMouse = activePopupWidget->underMouse();
+            if (activePopupWidget != m_widget || (!underMouse && qt_button_down)) {
+                // If active popup menu is not the first-level popup menu then we must emulate enter/leave events,
+                // because first-level popup menu grabs the mouse and enter/leave events are delivered only to it
+                // by QPA. Make an exception for first-level popup menu when the mouse button is pressed on widget.
+                if (underMouse != reallyUnderMouse) {
+                    if (reallyUnderMouse) {
+                        QApplicationPrivate::dispatchEnterLeave(receiver, Q_NULLPTR, event->screenPos());
+                        qt_last_mouse_receiver = receiver;
+                    } else {
+                        QApplicationPrivate::dispatchEnterLeave(Q_NULLPTR, qt_last_mouse_receiver, event->screenPos());
+                        qt_last_mouse_receiver = receiver;
+                        receiver = activePopupWidget;
+                    }
+                }
+            } else if (!reallyUnderMouse) {
+                alien = Q_NULLPTR;
+            }
+#endif
+
             QMouseEvent e(event->type(), widgetPos, event->windowPos(), event->screenPos(),
                           event->button(), event->buttons(), event->modifiers(), event->source());
             e.setTimestamp(event->timestamp());
@@ -457,7 +487,7 @@ void QWidgetWindow::handleMouseEvent(QMouseEvent *event)
             case QEvent::MouseButtonPress:
             case QEvent::MouseButtonDblClick:
             case QEvent::MouseButtonRelease:
-                popup->close();
+                activePopupWidget->close();
                 break;
             default:
                 break;
@@ -503,7 +533,7 @@ void QWidgetWindow::handleMouseEvent(QMouseEvent *event)
         } else if (event->type() == contextMenuTrigger
                    && event->button() == Qt::RightButton
                    && (openPopupCount == oldOpenPopupCount)) {
-            QWidget *popupEvent = popup;
+            QWidget *popupEvent = activePopupWidget;
             if (qt_button_down)
                 popupEvent = qt_button_down;
             else if(popupChild)
diff --git a/tests/auto/widgets/widgets/qmenu/tst_qmenu.cpp b/tests/auto/widgets/widgets/qmenu/tst_qmenu.cpp
index b3f9c54f24f..20f17f6e9e4 100644
--- a/tests/auto/widgets/widgets/qmenu/tst_qmenu.cpp
+++ b/tests/auto/widgets/widgets/qmenu/tst_qmenu.cpp
@@ -110,6 +110,9 @@ private slots:
     void QTBUG7411_submenus_activate();
     void QTBUG30595_rtl_submenu();
     void QTBUG20403_nested_popup_on_shortcut_trigger();
+#ifndef QT_NO_CURSOR
+    void QTBUG47515_widgetActionEnterLeave();
+#endif
     void QTBUG_10735_crashWithDialog();
 #ifdef Q_OS_MAC
     void QTBUG_37933_ampersands_data();
@@ -1070,6 +1073,112 @@ void tst_QMenu::QTBUG20403_nested_popup_on_shortcut_trigger()
     QVERIFY(!subsub1.isVisible());
 }
 
+class MyWidget : public QWidget
+{
+public:
+    MyWidget(QWidget *parent) :
+        QWidget(parent),
+        move(0), enter(0), leave(0)
+    {
+        setMinimumSize(100, 100);
+        setMouseTracking(true);
+    }
+
+    bool event(QEvent *e) Q_DECL_OVERRIDE
+    {
+        switch (e->type()) {
+        case QEvent::MouseMove:
+            ++move;
+            break;
+        case QEvent::Enter:
+            ++enter;
+            break;
+        case QEvent::Leave:
+            ++leave;
+            break;
+        default:
+            break;
+        }
+        return QWidget::event(e);
+    }
+
+    int move, enter, leave;
+};
+
+#ifndef QT_NO_CURSOR
+void tst_QMenu::QTBUG47515_widgetActionEnterLeave()
+{
+    if (QGuiApplication::platformName() == QLatin1String("cocoa"))
+        QSKIP("This test fails on OS X on CI");
+
+    const QPoint center = QGuiApplication::primaryScreen()->availableGeometry().center();
+    const QPoint cursorPos = center - QPoint(100, 100);
+
+    QScopedPointer<QMenu> menu1(new QMenu("Menu1"));
+    QScopedPointer<QMenu> menu2(new QMenu("Menu2"));
+
+    QWidgetAction *wA1 = new QWidgetAction(menu1.data());
+    MyWidget *w1 = new MyWidget(menu1.data());
+    wA1->setDefaultWidget(w1);
+
+    QWidgetAction *wA2 = new QWidgetAction(menu2.data());
+    MyWidget *w2 = new MyWidget(menu2.data());
+    wA2->setDefaultWidget(w2);
+
+    QAction *nextMenuAct = menu1->addMenu(menu2.data());
+
+    menu1->addAction(wA1);
+    menu2->addAction(wA2);
+
+    // Root menu
+    {
+        QCursor::setPos(cursorPos);
+        QCoreApplication::processEvents();
+
+        menu1->popup(center);
+        QVERIFY(QTest::qWaitForWindowExposed(menu1.data()));
+
+        QCursor::setPos(w1->mapToGlobal(w1->rect().center()));
+        QVERIFY(w1->isVisible());
+        QTRY_COMPARE(w1->leave, 0);
+        QTRY_COMPARE(w1->enter, 1);
+
+        // Check whether leave event is not delivered on mouse move
+        w1->move = 0;
+        QCursor::setPos(w1->mapToGlobal(w1->rect().center()) + QPoint(1, 1));
+        QTRY_COMPARE(w1->move, 1);
+        QTRY_COMPARE(w1->leave, 0);
+        QTRY_COMPARE(w1->enter, 1);
+
+        QCursor::setPos(cursorPos);
+        QTRY_COMPARE(w1->leave, 1);
+        QTRY_COMPARE(w1->enter, 1);
+    }
+
+    // Submenu
+    {
+        menu1->setActiveAction(nextMenuAct);
+        QVERIFY(QTest::qWaitForWindowExposed(menu2.data()));
+
+        QCursor::setPos(w2->mapToGlobal(w2->rect().center()));
+        QVERIFY(w2->isVisible());
+        QTRY_COMPARE(w2->leave, 0);
+        QTRY_COMPARE(w2->enter, 1);
+
+        // Check whether leave event is not delivered on mouse move
+        w2->move = 0;
+        QCursor::setPos(w2->mapToGlobal(w2->rect().center()) + QPoint(1, 1));
+        QTRY_COMPARE(w2->move, 1);
+        QTRY_COMPARE(w2->leave, 0);
+        QTRY_COMPARE(w2->enter, 1);
+
+        QCursor::setPos(cursorPos);
+        QTRY_COMPARE(w2->leave, 1);
+        QTRY_COMPARE(w2->enter, 1);
+    }
+}
+#endif // !QT_NO_CURSOR
+
 class MyMenu : public QMenu
 {
     Q_OBJECT
-- 
GitLab