From 4585cacaa9f9329a10aaf13a449151f5e9bc7a2c Mon Sep 17 00:00:00 2001 From: Volodymyr Zibarov Date: Fri, 3 May 2024 15:58:50 +0300 Subject: [PATCH] Add way to override when to show context menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Qt::ContextMenuTrigger enum used with QStyleHints::setContextMenuTrigger() to override default platform behavior when to trigger context menu event. The default is to show context menu on mouse press on UNIX systems and on mouse release on Windows. Give developer a possibility to override platform default behavior to make cross platform application that behaves the same way on all platforms Task-number: QTBUG-93486 Change-Id: Ic832d3d8a7c355a8adb46868fff9cfd19988cf3c Reviewed-by: Tor Arne Vestbø --- src/corelib/global/qnamespace.h | 6 +++ src/corelib/global/qnamespace.qdoc | 12 ++++++ src/gui/kernel/qguiapplication.cpp | 9 +++++ src/gui/kernel/qguiapplication_p.h | 2 + src/gui/kernel/qstylehints.cpp | 34 ++++++++++++++++ src/gui/kernel/qstylehints.h | 5 +++ src/gui/kernel/qstylehints_p.h | 1 + src/gui/kernel/qwindow.cpp | 8 ++-- src/widgets/kernel/qwidgetwindow.cpp | 9 ++--- .../widgets/kernel/qwidget/tst_qwidget.cpp | 40 +++++++++++++++++++ 10 files changed, 115 insertions(+), 11 deletions(-) diff --git a/src/corelib/global/qnamespace.h b/src/corelib/global/qnamespace.h index 2398c0a1a49..1569577b129 100644 --- a/src/corelib/global/qnamespace.h +++ b/src/corelib/global/qnamespace.h @@ -1356,6 +1356,11 @@ namespace Qt { PreventContextMenu }; + enum class ContextMenuTrigger { + Press, + Release, + }; + enum InputMethodQuery { ImEnabled = 0x1, ImCursorRectangle = 0x2, @@ -1731,6 +1736,7 @@ namespace Qt { Q_ENUM_NS(ScrollBarPolicy) Q_ENUM_NS(FocusPolicy) Q_ENUM_NS(ContextMenuPolicy) + Q_ENUM_NS(ContextMenuTrigger) Q_ENUM_NS(ArrowType) Q_ENUM_NS(ToolButtonStyle) Q_ENUM_NS(PenStyle) diff --git a/src/corelib/global/qnamespace.qdoc b/src/corelib/global/qnamespace.qdoc index ddfade675a3..b2ec64f435c 100644 --- a/src/corelib/global/qnamespace.qdoc +++ b/src/corelib/global/qnamespace.qdoc @@ -2086,6 +2086,18 @@ \value CustomContextMenu the widget emits the QWidget::customContextMenuRequested() signal. */ +/*! + \enum Qt::ContextMenuTrigger + \since 6.8 + + This enum type defines the mouse event used to trigger a context menu event. + + \value Press context menu on mouse press event, default on UNIX systems. + \value Release context menu on mouse release event, default on Windows. + + \sa QStyleHints::contextMenuTrigger +*/ + /*! \enum Qt::FocusPolicy diff --git a/src/gui/kernel/qguiapplication.cpp b/src/gui/kernel/qguiapplication.cpp index c97374e9757..f2b914f203d 100644 --- a/src/gui/kernel/qguiapplication.cpp +++ b/src/gui/kernel/qguiapplication.cpp @@ -3447,6 +3447,15 @@ void QGuiApplicationPrivate::updatePalette() } } +QEvent::Type QGuiApplicationPrivate::contextMenuEventType() +{ + switch (QGuiApplication::styleHints()->contextMenuTrigger()) { + case Qt::ContextMenuTrigger::Press: return QEvent::MouseButtonPress; + case Qt::ContextMenuTrigger::Release: return QEvent::MouseButtonRelease; + } + return QEvent::None; +} + void QGuiApplicationPrivate::clearPalette() { delete app_pal; diff --git a/src/gui/kernel/qguiapplication_p.h b/src/gui/kernel/qguiapplication_p.h index cca79534fc7..39c490c5813 100644 --- a/src/gui/kernel/qguiapplication_p.h +++ b/src/gui/kernel/qguiapplication_p.h @@ -323,6 +323,8 @@ public: static void updatePalette(); + static QEvent::Type contextMenuEventType(); + protected: virtual void handleThemeChanged(); diff --git a/src/gui/kernel/qstylehints.cpp b/src/gui/kernel/qstylehints.cpp index 0d15d114ec5..73c61997330 100644 --- a/src/gui/kernel/qstylehints.cpp +++ b/src/gui/kernel/qstylehints.cpp @@ -442,6 +442,40 @@ void QStyleHints::setShowShortcutsInContextMenus(bool s) } } +/*! + \property QStyleHints::contextMenuTrigger + \since 6.8 + \brief mouse event used to trigger a context menu event. + + The default on UNIX systems is to show context menu on mouse button press event, while on + Windows it is the mouse button release event. This property can be used to override the default + platform behavior. + + \note Developers must use this property with great care, as it changes the default interaction + mode that their users will expect on the platform that they are running on. + + \sa Qt::ContextMenuTrigger +*/ +Qt::ContextMenuTrigger QStyleHints::contextMenuTrigger() const +{ + Q_D(const QStyleHints); + if (d->m_contextMenuTrigger == -1) { + return themeableHint(QPlatformTheme::ContextMenuOnMouseRelease).toBool() + ? Qt::ContextMenuTrigger::Release + : Qt::ContextMenuTrigger::Press; + } + return Qt::ContextMenuTrigger(d->m_contextMenuTrigger); +} + +void QStyleHints::setContextMenuTrigger(Qt::ContextMenuTrigger contextMenuTrigger) +{ + Q_D(QStyleHints); + const Qt::ContextMenuTrigger currentTrigger = this->contextMenuTrigger(); + d->m_contextMenuTrigger = int(contextMenuTrigger); + if (currentTrigger != contextMenuTrigger) + emit contextMenuTriggerChanged(contextMenuTrigger); +} + /*! \property QStyleHints::passwordMaskDelay \brief the time, in milliseconds, a typed letter is displayed unshrouded diff --git a/src/gui/kernel/qstylehints.h b/src/gui/kernel/qstylehints.h index 89813830603..97ef59f3cf5 100644 --- a/src/gui/kernel/qstylehints.h +++ b/src/gui/kernel/qstylehints.h @@ -36,6 +36,8 @@ class Q_GUI_EXPORT QStyleHints : public QObject Q_PROPERTY(bool showIsMaximized READ showIsMaximized STORED false CONSTANT FINAL) Q_PROPERTY(bool showShortcutsInContextMenus READ showShortcutsInContextMenus WRITE setShowShortcutsInContextMenus NOTIFY showShortcutsInContextMenusChanged FINAL) + Q_PROPERTY(Qt::ContextMenuTrigger contextMenuTrigger READ contextMenuTrigger WRITE + setContextMenuTrigger NOTIFY contextMenuTriggerChanged FINAL) Q_PROPERTY(int startDragDistance READ startDragDistance NOTIFY startDragDistanceChanged FINAL) Q_PROPERTY(int startDragTime READ startDragTime NOTIFY startDragTimeChanged FINAL) Q_PROPERTY(int startDragVelocity READ startDragVelocity STORED false CONSTANT FINAL) @@ -80,6 +82,8 @@ public: bool showIsMaximized() const; bool showShortcutsInContextMenus() const; void setShowShortcutsInContextMenus(bool showShortcutsInContextMenus); + Qt::ContextMenuTrigger contextMenuTrigger() const; + void setContextMenuTrigger(Qt::ContextMenuTrigger contextMenuTrigger); int passwordMaskDelay() const; QChar passwordMaskCharacter() const; qreal fontSmoothingGamma() const; @@ -108,6 +112,7 @@ Q_SIGNALS: void tabFocusBehaviorChanged(Qt::TabFocusBehavior tabFocusBehavior); void useHoverEffectsChanged(bool useHoverEffects); void showShortcutsInContextMenusChanged(bool); + void contextMenuTriggerChanged(Qt::ContextMenuTrigger contextMenuTrigger); void wheelScrollLinesChanged(int scrollLines); void mouseQuickSelectionThresholdChanged(int threshold); void colorSchemeChanged(Qt::ColorScheme colorScheme); diff --git a/src/gui/kernel/qstylehints_p.h b/src/gui/kernel/qstylehints_p.h index 2b3979512a1..497bf95cbf4 100644 --- a/src/gui/kernel/qstylehints_p.h +++ b/src/gui/kernel/qstylehints_p.h @@ -35,6 +35,7 @@ public: int m_tabFocusBehavior = -1; int m_uiEffects = -1; int m_showShortcutsInContextMenus = -1; + int m_contextMenuTrigger = -1; int m_wheelScrollLines = -1; int m_mouseQuickSelectionThreshold = -1; int m_mouseDoubleClickDistance = -1; diff --git a/src/gui/kernel/qwindow.cpp b/src/gui/kernel/qwindow.cpp index b40fd7e8e84..7c885032c76 100644 --- a/src/gui/kernel/qwindow.cpp +++ b/src/gui/kernel/qwindow.cpp @@ -2654,16 +2654,14 @@ bool QWindow::event(QEvent *ev) This logic could be simplified by always synthesizing events in QGuiApplicationPrivate, or perhaps even in each QPA plugin. See QTBUG-93486. */ - static const QEvent::Type contextMenuTrigger = - QGuiApplicationPrivate::platformTheme()->themeHint(QPlatformTheme::ContextMenuOnMouseRelease).toBool() ? - QEvent::MouseButtonRelease : QEvent::MouseButtonPress; auto asMouseEvent = [](QEvent *ev) { const auto t = ev->type(); return t == QEvent::MouseButtonPress || t == QEvent::MouseButtonRelease ? static_cast(ev) : nullptr ; }; - if (QMouseEvent *me = asMouseEvent(ev); me && - ev->type() == contextMenuTrigger && me->button() == Qt::RightButton) { + if (QMouseEvent *me = asMouseEvent(ev); + me && ev->type() == QGuiApplicationPrivate::contextMenuEventType() + && me->button() == Qt::RightButton) { QContextMenuEvent e(QContextMenuEvent::Mouse, me->position().toPoint(), me->globalPosition().toPoint(), me->modifiers()); QGuiApplication::sendEvent(this, &e); diff --git a/src/widgets/kernel/qwidgetwindow.cpp b/src/widgets/kernel/qwidgetwindow.cpp index e7f0a840041..03dde9ca69e 100644 --- a/src/widgets/kernel/qwidgetwindow.cpp +++ b/src/widgets/kernel/qwidgetwindow.cpp @@ -505,9 +505,6 @@ void QWidgetWindow::handleNonClientAreaMouseEvent(QMouseEvent *e) void QWidgetWindow::handleMouseEvent(QMouseEvent *event) { - static const QEvent::Type contextMenuTrigger = - QGuiApplicationPrivate::platformTheme()->themeHint(QPlatformTheme::ContextMenuOnMouseRelease).toBool() ? - QEvent::MouseButtonRelease : QEvent::MouseButtonPress; if (QApplicationPrivate::inPopupMode()) { QPointer activePopupWidget = QApplication::activePopupWidget(); QPointF mapped = event->position(); @@ -624,7 +621,7 @@ void QWidgetWindow::handleMouseEvent(QMouseEvent *event) } qt_replay_popup_mouse_event = false; #ifndef QT_NO_CONTEXTMENU - } else if (event->type() == contextMenuTrigger + } else if (event->type() == QGuiApplicationPrivate::contextMenuEventType() && event->button() == Qt::RightButton && (openPopupCount == oldOpenPopupCount)) { QWidget *receiver = activePopupWidget; @@ -637,7 +634,6 @@ void QWidgetWindow::handleMouseEvent(QMouseEvent *event) QApplication::forwardEvent(receiver, &e, event); } #else - Q_UNUSED(contextMenuTrigger); Q_UNUSED(oldOpenPopupCount); } #endif @@ -684,7 +680,8 @@ void QWidgetWindow::handleMouseEvent(QMouseEvent *event) event->setAccepted(translated.isAccepted()); } #ifndef QT_NO_CONTEXTMENU - if (event->type() == contextMenuTrigger && event->button() == Qt::RightButton + if (event->type() == QGuiApplicationPrivate::contextMenuEventType() + && event->button() == Qt::RightButton && m_widget->rect().contains(event->position().toPoint())) { QContextMenuEvent e(QContextMenuEvent::Mouse, mapped, event->globalPosition().toPoint(), event->modifiers()); QGuiApplication::forwardEvent(receiver, &e, event); diff --git a/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp b/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp index 3447c9af282..c68bb74f6e6 100644 --- a/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp +++ b/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp @@ -477,6 +477,10 @@ private slots: void reparentWindowHandles_data(); void reparentWindowHandles(); +#ifndef QT_NO_CONTEXTMENU + void contextMenuTrigger(); +#endif + private: const QString m_platform; QSize m_testWidgetSize; @@ -14040,5 +14044,41 @@ void tst_QWidget::reparentWindowHandles() } } +#ifndef QT_NO_CONTEXTMENU +void tst_QWidget::contextMenuTrigger() +{ + class ContextMenuWidget : public QWidget + { + public: + int events = 0; + + protected: + void contextMenuEvent(QContextMenuEvent *) override { ++events; } + }; + + const Qt::ContextMenuTrigger wasTrigger = QGuiApplication::styleHints()->contextMenuTrigger(); + auto restoreTriggerGuard = qScopeGuard([wasTrigger]{ + QGuiApplication::styleHints()->setContextMenuTrigger(wasTrigger); + }); + + ContextMenuWidget widget; + widget.show(); + QVERIFY(!qApp->topLevelWindows().empty()); + auto *window = qApp->topLevelWindows()[0]; + QVERIFY(window); + QCOMPARE(widget.events, 0); + QGuiApplication::styleHints()->setContextMenuTrigger(Qt::ContextMenuTrigger::Press); + QTest::mousePress(window, Qt::RightButton); + QCOMPARE(widget.events, 1); + QTest::mouseRelease(window, Qt::RightButton); + QCOMPARE(widget.events, 1); + QGuiApplication::styleHints()->setContextMenuTrigger(Qt::ContextMenuTrigger::Release); + QTest::mousePress(window, Qt::RightButton); + QCOMPARE(widget.events, 1); + QTest::mouseRelease(window, Qt::RightButton); + QCOMPARE(widget.events, 2); +} +#endif + QTEST_MAIN(tst_QWidget) #include "tst_qwidget.moc"