summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVolker Hilsheimer <volker.hilsheimer@qt.io>2020-06-15 00:23:41 +0200
committerVolker Hilsheimer <volker.hilsheimer@qt.io>2020-07-02 09:43:04 +0200
commita75f423e26f52e34334ad2aef551a324651fb7cd (patch)
tree0e33360b9b373bce787716b1eeb73d9c801c3871
parent4cb8434344fa0ee021c11200b13976a9b7d0610e (diff)
downloadqtbase-a75f423e26f52e34334ad2aef551a324651fb7cd.tar.gz
Fix delivery of MouseMove events to newly opened popup windows
Amend d934fd7f54eae24ea3f719890e2c4dbbc445049d, which was too naive in assuming that any change to the popup stack while a popup had been pressed into should result in mouse move events to be delivered without buttons. Instead, add a new flag that is set explicitly when the qt_popup_down widget is closed, and remove buttons from the move move events only when that flag is set. Add the sorely missing test case as well, even if we have to accept that not all behavior can be tested reliably. Ie. on macOS, the simulated mouse event differs from the event we do get from the QPA plugin or the system; on Xcb, some of the behavior depends on the window manager. This is something we could try to clean up for Qt 6. Change-Id: Ibf0a0a6fb7d401915057365788947e5a35aa20c3 Fixes: QTBUG-84926 Task-number: QTBUG-82538 Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Liang Qi <liang.qi@qt.io> (cherry picked from commit ab6861b01ff9c14fd1648cb725da17d1ebf3faea) Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
-rw-r--r--src/widgets/kernel/qapplication.cpp3
-rw-r--r--src/widgets/kernel/qwidgetwindow.cpp6
-rw-r--r--tests/auto/widgets/kernel/qwidget_window/BLACKLIST3
-rw-r--r--tests/auto/widgets/kernel/qwidget_window/tst_qwidget_window.cpp186
4 files changed, 197 insertions, 1 deletions
diff --git a/src/widgets/kernel/qapplication.cpp b/src/widgets/kernel/qapplication.cpp
index 950e424a37..55c21a824a 100644
--- a/src/widgets/kernel/qapplication.cpp
+++ b/src/widgets/kernel/qapplication.cpp
@@ -3714,6 +3714,7 @@ static void grabForPopup(QWidget *popup)
extern QWidget *qt_popup_down;
extern bool qt_replay_popup_mouse_event;
+extern bool qt_popup_down_closed;
void QApplicationPrivate::closePopup(QWidget *popup)
{
@@ -3723,12 +3724,14 @@ void QApplicationPrivate::closePopup(QWidget *popup)
if (popup == qt_popup_down) {
qt_button_down = nullptr;
+ qt_popup_down_closed = true;
qt_popup_down = nullptr;
}
if (QApplicationPrivate::popupWidgets->count() == 0) { // this was the last popup
delete QApplicationPrivate::popupWidgets;
QApplicationPrivate::popupWidgets = nullptr;
+ qt_popup_down_closed = false;
if (popupGrabOk) {
popupGrabOk = false;
diff --git a/src/widgets/kernel/qwidgetwindow.cpp b/src/widgets/kernel/qwidgetwindow.cpp
index 40d8311c25..f612da0930 100644
--- a/src/widgets/kernel/qwidgetwindow.cpp
+++ b/src/widgets/kernel/qwidgetwindow.cpp
@@ -62,6 +62,7 @@ Q_WIDGETS_EXPORT QWidget *qt_button_down = nullptr; // widget got last button-do
// popup control
QWidget *qt_popup_down = nullptr; // popup that contains the pressed widget
extern int openPopupCount;
+bool qt_popup_down_closed = false; // qt_popup_down has been closed
bool qt_replay_popup_mouse_event = false;
extern bool qt_try_modal(QWidget *widget, QEvent::Type type);
@@ -526,6 +527,7 @@ void QWidgetWindow::handleMouseEvent(QMouseEvent *event)
case QEvent::MouseButtonDblClick:
qt_button_down = popupChild;
qt_popup_down = activePopupWidget;
+ qt_popup_down_closed = false;
break;
case QEvent::MouseButtonRelease:
releaseAfter = true;
@@ -570,7 +572,7 @@ void QWidgetWindow::handleMouseEvent(QMouseEvent *event)
if ((event->type() != QEvent::MouseButtonPress)
|| !(event->flags().testFlag(Qt::MouseEventCreatedDoubleClick))) {
// if the widget that was pressed is gone, then deliver move events without buttons
- const auto buttons = event->type() == QEvent::MouseMove && qt_button_down == nullptr
+ const auto buttons = event->type() == QEvent::MouseMove && qt_popup_down_closed
? Qt::NoButton : event->buttons();
QMouseEvent e(event->type(), widgetPos, event->windowPos(), event->screenPos(),
event->button(), buttons, event->modifiers(), event->source());
@@ -643,11 +645,13 @@ void QWidgetWindow::handleMouseEvent(QMouseEvent *event)
if (releaseAfter) {
qt_button_down = nullptr;
+ qt_popup_down_closed = false;
qt_popup_down = nullptr;
}
return;
}
+ qt_popup_down_closed = false;
// modal event handling
if (QApplicationPrivate::instance()->modalState() && !qt_try_modal(m_widget, event->type()))
return;
diff --git a/tests/auto/widgets/kernel/qwidget_window/BLACKLIST b/tests/auto/widgets/kernel/qwidget_window/BLACKLIST
index d3bfaba433..70a7889257 100644
--- a/tests/auto/widgets/kernel/qwidget_window/BLACKLIST
+++ b/tests/auto/widgets/kernel/qwidget_window/BLACKLIST
@@ -2,3 +2,6 @@
# QTBUG-66345
opensuse-42.3
ubuntu-16.04
+
+[mouseMoveWithPopup]
+winrt
diff --git a/tests/auto/widgets/kernel/qwidget_window/tst_qwidget_window.cpp b/tests/auto/widgets/kernel/qwidget_window/tst_qwidget_window.cpp
index 72fa32a1b1..b11faef30a 100644
--- a/tests/auto/widgets/kernel/qwidget_window/tst_qwidget_window.cpp
+++ b/tests/auto/widgets/kernel/qwidget_window/tst_qwidget_window.cpp
@@ -126,6 +126,9 @@ private slots:
void QTBUG_56277_resize_on_showEvent();
+ void mouseMoveWithPopup_data();
+ void mouseMoveWithPopup();
+
private:
QSize m_testWidgetSize;
const int m_fuzz;
@@ -1337,5 +1340,188 @@ void tst_QWidget_window::QTBUG_56277_resize_on_showEvent()
QVERIFY(geometry.top() > topmostY || geometry.left() > screen->availableGeometry().left());
}
+void tst_QWidget_window::mouseMoveWithPopup_data()
+{
+ QTest::addColumn<Qt::WindowType>("windowType");
+
+ QTest::addRow("Dialog") << Qt::Dialog;
+ QTest::addRow("Popup") << Qt::Popup;
+}
+
+void tst_QWidget_window::mouseMoveWithPopup()
+{
+ QFETCH(Qt::WindowType, windowType);
+
+ class Window : public QWidget
+ {
+ public:
+ Window(QWidget *parent = nullptr, Qt::WindowFlags flags = {})
+ : QWidget(parent, flags|Qt::CustomizeWindowHint|Qt::FramelessWindowHint)
+ {}
+
+ QSize sizeHint() const
+ {
+ if (parent())
+ return QSize(150, 100);
+ return QSize(250, 250);
+ }
+
+ Window *popup = nullptr;
+ Qt::WindowType type = Qt::Popup;
+ int mousePressCount = 0;
+ int mouseMoveCount = 0;
+ int mouseReleaseCount = 0;
+ void resetCounters()
+ {
+ mousePressCount = 0;
+ mouseMoveCount = 0;
+ mouseReleaseCount = 0;
+ }
+ protected:
+ void mousePressEvent(QMouseEvent *event)
+ {
+ ++mousePressCount;
+
+ if (event->button() == Qt::RightButton) {
+ if (!popup)
+ popup = new Window(this, type);
+ popup->move(event->globalPos());
+ popup->show();
+ if (!QTest::qWaitForWindowExposed(popup)) {
+ delete popup;
+ popup = nullptr;
+ QSKIP("Failed to expose popup window!");
+ }
+ } else {
+ QWidget::mousePressEvent(event);
+ }
+ }
+ void mouseReleaseEvent(QMouseEvent *event)
+ {
+ ++mouseReleaseCount;
+ QWidget::mouseReleaseEvent(event);
+ }
+ void mouseMoveEvent(QMouseEvent *event)
+ {
+ ++mouseMoveCount;
+ QWidget::mouseMoveEvent(event);
+ }
+ };
+ Window topLevel;
+ topLevel.setObjectName("topLevel");
+ topLevel.type = windowType;
+ topLevel.show();
+ if (!QTest::qWaitForWindowExposed(&topLevel))
+ QSKIP("Failed to expose window!");
+
+ QCOMPARE(QApplication::activePopupWidget(), nullptr);
+ QCOMPARE(QApplication::activeWindow(), &topLevel);
+
+ QPoint mousePos = topLevel.geometry().center();
+ QWindow *window = nullptr;
+ Qt::MouseButtons buttons = {};
+ auto mouseAction = [&](Qt::MouseButton button, QPoint offset = {}) -> QEvent::Type
+ {
+ QEvent::Type type;
+ if (offset != QPoint()) {
+ type = QEvent::MouseMove;
+ } else if (buttons & button) {
+ type = QEvent::MouseButtonRelease;
+ buttons &= ~button;
+ } else {
+ Q_ASSERT(button != Qt::NoButton);
+ type = QEvent::MouseButtonPress;
+ buttons |= button;
+ window = QApplication::activeWindow()->windowHandle();
+ }
+
+ mousePos += offset;
+
+ if (!window)
+ return QEvent::None;
+
+ bool result = QWindowSystemInterface::handleMouseEvent(window, window->mapFromGlobal(mousePos),
+ mousePos, buttons, button, type);
+ QCoreApplication::processEvents();
+ if (type == QEvent::MouseButtonRelease && buttons == Qt::NoButton)
+ window = nullptr;
+
+ if (!result)
+ return QEvent::None;
+ return type;
+ };
+
+ QCOMPARE(mouseAction(Qt::RightButton), QEvent::MouseButtonPress);
+ QCOMPARE(topLevel.mousePressCount, 1);
+ QVERIFY(topLevel.popup);
+ QCOMPARE(topLevel.popup->mousePressCount, 0);
+ topLevel.popup->setObjectName(windowType == Qt::Popup ? "Popup" : "Dialog");
+ QCOMPARE(QApplication::activePopupWidget(), windowType == Qt::Popup ? topLevel.popup : nullptr);
+ // if popup, then popup gets the mouse move even though it didn't get any press
+ QCOMPARE(mouseAction(Qt::NoButton, QPoint(10, 10)), QEvent::MouseMove);
+ QCOMPARE(topLevel.mouseMoveCount, windowType == Qt::Popup ? 0 : 1);
+ QCOMPARE(topLevel.popup->mouseMoveCount, windowType == Qt::Popup ? 1 : 0);
+ // if popup, then popup gets the release even though it didn't get any press
+ QCOMPARE(mouseAction(Qt::RightButton), QEvent::MouseButtonRelease);
+ QCOMPARE(topLevel.mouseReleaseCount, windowType == Qt::Popup ? 0 : 1);
+ QCOMPARE(topLevel.popup->mouseReleaseCount, windowType == Qt::Popup ? 1 : 0);
+
+ Q_ASSERT(buttons == Qt::NoButton);
+ topLevel.resetCounters();
+ topLevel.popup->resetCounters();
+
+ // nested popup, same procedure
+ QCOMPARE(mouseAction(Qt::RightButton), QEvent::MouseButtonPress);
+ QVERIFY(topLevel.popup);
+ QCOMPARE(topLevel.popup->mousePressCount, 1);
+ QVERIFY(topLevel.popup->popup);
+ topLevel.popup->popup->setObjectName("NestedPopup");
+ QCOMPARE(QApplication::activePopupWidget(), topLevel.popup->popup);
+ QCOMPARE(topLevel.popup->popup->mousePressCount, 0);
+
+ // nested popup is always a popup and grabs the mouse, so first popup gets nothing
+ QCOMPARE(mouseAction(Qt::NoButton, QPoint(10, 10)), QEvent::MouseMove);
+ QCOMPARE(topLevel.popup->mouseMoveCount, 0);
+ QCOMPARE(topLevel.popup->popup->mouseMoveCount, 1);
+
+ // nested popup gets the release, as before
+ QCOMPARE(mouseAction(Qt::RightButton), QEvent::MouseButtonRelease);
+ QCOMPARE(topLevel.popup->mouseReleaseCount, 0);
+ QCOMPARE(topLevel.popup->popup->mouseReleaseCount, 1);
+
+ Q_ASSERT(buttons == Qt::NoButton);
+
+ // move mouse back into first popup
+ mouseAction({}, QPoint(-15, -15));
+ QVERIFY(!topLevel.popup->popup->geometry().contains(mousePos));
+ QVERIFY(topLevel.popup->geometry().contains(mousePos));
+
+ topLevel.popup->resetCounters();
+ topLevel.popup->popup->resetCounters();
+
+ // closing the nested popup by clicking into the first popup/dialog; the nested popup gets the press
+ QCOMPARE(mouseAction(Qt::LeftButton), QEvent::MouseButtonPress);
+ QCOMPARE(topLevel.popup->popup->mousePressCount, 1);
+ QVERIFY(!topLevel.popup->popup->isVisible());
+ QCOMPARE(QApplication::activePopupWidget(), windowType == Qt::Popup ? topLevel.popup : nullptr);
+ QCOMPARE(QApplication::activeWindow(), windowType == Qt::Popup ? &topLevel : topLevel.popup);
+
+ // the move event following a press that closed the active popup should NOT be delivered to the first popup
+ QCOMPARE(mouseAction({}, QPoint(-10, -10)), QEvent::MouseMove);
+ // dialogs might or might not get the event - platform specific behavior in Qt 5
+ if (topLevel.popup->mouseMoveCount != 0)
+ QEXPECT_FAIL("Dialog", "Platform specific behavior", Continue);
+ QCOMPARE(topLevel.popup->mouseMoveCount, 0);
+ QCOMPARE(topLevel.popup->popup->mouseMoveCount, 0);
+
+ // but the release event will still be delivered to the first popup - dialogs might not get it
+ QCOMPARE(mouseAction(Qt::LeftButton), QEvent::MouseButtonRelease);
+ if (topLevel.popup->mouseMoveCount != 1
+ && (QGuiApplication::platformName().startsWith(QLatin1String("xcb"), Qt::CaseInsensitive)
+ || QGuiApplication::platformName().startsWith(QLatin1String("offscreen"), Qt::CaseInsensitive)))
+ QEXPECT_FAIL("Dialog", "Platform specific behavior", Continue);
+ QCOMPARE(topLevel.popup->mouseReleaseCount, 1);
+}
+
QTEST_MAIN(tst_QWidget_window)
#include "tst_qwidget_window.moc"