ContextMenu: defer creation of Menu until it's shown

This enables adding a Cut/Copy/Paste context menu to our text editing
controls without a Menu being created at startup for each control.

Fixes: QTBUG-132265
Pick-to: 6.9
Change-Id: I2fbbce457208cfeb0df8f043746615b7c52f65e0
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
This commit is contained in:
Mitch Curtis 2024-12-17 14:03:08 +08:00
parent b7e341ec5a
commit 5a987204a6
10 changed files with 226 additions and 34 deletions

View File

@ -0,0 +1,17 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick.Controls
//! [root]
Pane {
anchors.fill: parent
ContextMenu.menu: Menu {
// This prevents lazy creation of the Menu.
id: myMenu
// ...
}
}
//! [root]

View File

@ -9,6 +9,7 @@
#include <QtQml/qqmlinfo.h>
#include <QtQuick/qquickwindow.h>
#include <QtQuick/private/qquickitem_p.h>
#include <QtQuickTemplates2/private/qquickdeferredexecute_p_p.h>
#include <QtQuickTemplates2/private/qquickmenu_p.h>
QT_BEGIN_NAMESPACE
@ -36,6 +37,22 @@ Q_STATIC_LOGGING_CATEGORY(lcContextMenu, "qt.quick.controls.contextmenu")
context menus have data in common. For example:
\snippet qtquickcontrols-contextmenu-shared.qml file
\section1 Performance
ContextMenu lazily creates its \c Menu only when it's requested. If it
wasn't for this optimization, the \c Menu would be created when the
containing component is being loaded, which is typically at application
startup.
It is recommended not to give the \c Menu assigned to ContextMenu's \l menu
property an id when it's defined where it's assigned. Doing so prevents
this optimization. For example:
\snippet qtquickcontrols-contextmenu-id.qml root
The example in the \l {Sharing context menus} section works because the
\c Menu is defined separately from its assignment.
*/
/*!
@ -66,11 +83,47 @@ public:
return attachedObject->d_func();
}
void cancelMenu();
void executeMenu(bool complete = false);
bool isRequestedSignalConnected();
QPointer<QQuickMenu> menu;
QQuickDeferredPointer<QQuickMenu> menu;
bool complete = false;
};
static const QString menuPropertyName = QStringLiteral("menu");
void QQuickContextMenuPrivate::cancelMenu()
{
Q_Q(QQuickContextMenu);
quickCancelDeferred(q, menuPropertyName);
}
void QQuickContextMenuPrivate::executeMenu(bool complete)
{
Q_Q(QQuickContextMenu);
if (menu.wasExecuted())
return;
QQmlEngine *engine = nullptr;
auto *parentItem = qobject_cast<QQuickItem *>(q->parent());
if (parentItem) {
engine = qmlEngine(parentItem);
// In most cases, the above line will work, but if it doesn't,
// it could be because we're attached to the contentItem of the window.
// In that case, we'll be created before the contentItem has a context
// and hence an engine. However, the window will have one, so use that.
if (!engine)
engine = qmlEngine(parentItem->window());
}
if (!menu || complete)
quickBeginAttachedDeferred(q, menuPropertyName, menu, engine);
if (complete)
quickCompleteAttachedDeferred(q, menuPropertyName, menu, engine);
}
bool QQuickContextMenuPrivate::isRequestedSignalConnected()
{
Q_Q(QQuickContextMenu);
@ -107,7 +160,12 @@ QQuickContextMenu *QQuickContextMenu::qmlAttachedProperties(QObject *object)
*/
QQuickMenu *QQuickContextMenu::menu() const
{
Q_D(const QQuickContextMenu);
auto *d = const_cast<QQuickContextMenuPrivate *>(d_func());
if (!d->menu) {
qCDebug(lcContextMenu) << "creating menu via deferred execution"
<< "- is component complete:" << d->complete;
d->executeMenu(d->complete);
}
return d->menu;
}
@ -120,8 +178,23 @@ void QQuickContextMenu::setMenu(QQuickMenu *menu)
if (menu == d->menu)
return;
if (!d->menu.isExecuting())
d->cancelMenu();
d->menu = menu;
emit menuChanged();
if (!d->menu.isExecuting())
emit menuChanged();
}
void QQuickContextMenu::classBegin()
{
}
void QQuickContextMenu::componentComplete()
{
Q_D(QQuickContextMenu);
d->complete = true;
}
bool QQuickContextMenu::event(QEvent *event)

View File

@ -24,10 +24,12 @@ QT_BEGIN_NAMESPACE
class QQuickContextMenuPrivate;
class QQuickMenu;
class Q_QUICKTEMPLATES2_EXPORT QQuickContextMenu : public QObject
class Q_QUICKTEMPLATES2_EXPORT QQuickContextMenu : public QObject, public QQmlParserStatus
{
Q_OBJECT
Q_INTERFACES(QQmlParserStatus)
Q_PROPERTY(QQuickMenu *menu READ menu WRITE setMenu NOTIFY menuChanged FINAL)
Q_CLASSINFO("DeferredPropertyNames", "menu")
QML_NAMED_ELEMENT(ContextMenu)
QML_ATTACHED(QQuickContextMenu)
QML_UNCREATABLE("")
@ -50,6 +52,9 @@ protected:
private:
Q_DECLARE_PRIVATE(QQuickContextMenu)
void classBegin() override;
void componentComplete() override;
};
QT_END_NAMESPACE

View File

@ -109,11 +109,14 @@ static bool beginDeferred(QQmlEnginePrivate *enginePriv, const QQmlProperty &pro
}
void beginDeferred(QObject *object, const QString &property,
QQuickUntypedDeferredPointer *delegate, bool isOwnState)
QQuickUntypedDeferredPointer *delegate, bool isOwnState, QQmlEngine *engine)
{
QQmlData *data = QQmlData::get(object);
if (data && !data->deferredData.isEmpty() && !data->wasDeleted(object) && data->context) {
QQmlEnginePrivate *ep = QQmlEnginePrivate::get(data->context->engine());
// If object is an attached object, its QQmlData won't have a context, hence why we provide
// the option to pass an engine explicitly.
if (data && !data->deferredData.isEmpty() && !data->wasDeleted(object) && (data->context || engine)) {
QQmlEnginePrivate *ep = QQmlEnginePrivate::get(
data->context && data->context->engine() ? data->context->engine() : engine);
QQmlComponentPrivate::DeferredState state;
if (beginDeferred(ep, QQmlProperty(object, property), &state)) {
@ -137,7 +140,8 @@ void cancelDeferred(QObject *object, const QString &property)
cancelDeferred(data, QQmlProperty(object, property).index());
}
void completeDeferred(QObject *object, const QString &property, QQuickUntypedDeferredPointer *delegate)
void completeDeferred(QObject *object, const QString &property, QQuickUntypedDeferredPointer *delegate,
QQmlEngine *engine)
{
Q_UNUSED(property);
QQmlComponentPrivate::DeferredState *state = delegate->deferredState();
@ -157,7 +161,8 @@ void completeDeferred(QObject *object, const QString &property, QQuickUntypedDef
QQmlComponentPrivate::DeferredState localState = std::move(*state);
delegate->clearDeferredState();
QQmlEnginePrivate *ep = QQmlEnginePrivate::get(data->context->engine());
QQmlEnginePrivate *ep = QQmlEnginePrivate::get(
data->context && data->context->engine() ? data->context->engine() : engine);
QQmlComponentPrivate::completeDeferred(ep, &localState);
} else {
delegate->clearDeferredState();

View File

@ -27,9 +27,11 @@ class QString;
class QObject;
namespace QtQuickPrivate {
Q_QUICKTEMPLATES2_EXPORT void beginDeferred(QObject *object, const QString &property, QQuickUntypedDeferredPointer *delegate, bool isOwnState);
Q_QUICKTEMPLATES2_EXPORT void beginDeferred(QObject *object, const QString &property,
QQuickUntypedDeferredPointer *delegate, bool isOwnState, QQmlEngine *engine = nullptr);
Q_QUICKTEMPLATES2_EXPORT void cancelDeferred(QObject *object, const QString &property);
Q_QUICKTEMPLATES2_EXPORT void completeDeferred(QObject *object, const QString &property, QQuickUntypedDeferredPointer *delegate);
Q_QUICKTEMPLATES2_EXPORT void completeDeferred(QObject *object, const QString &property,
QQuickUntypedDeferredPointer *delegate, QQmlEngine *engine = nullptr);
}
template<typename T>
@ -42,6 +44,17 @@ void quickBeginDeferred(QObject *object, const QString &property, QQuickDeferred
delegate.setExecuting(false);
}
template<typename T>
void quickBeginAttachedDeferred(QObject *object, const QString &property,
QQuickDeferredPointer<T> &delegate, QQmlEngine *engine)
{
if (!QQmlVME::componentCompleteEnabled())
return;
QtQuickPrivate::beginDeferred(object, property, &delegate, delegate.setExecuting(true), engine);
delegate.setExecuting(false);
}
inline void quickCancelDeferred(QObject *object, const QString &property)
{
QtQuickPrivate::cancelDeferred(object, property);
@ -55,6 +68,15 @@ void quickCompleteDeferred(QObject *object, const QString &property, QQuickDefer
delegate.setExecuted();
}
template<typename T>
void quickCompleteAttachedDeferred(QObject *object, const QString &property,
QQuickDeferredPointer<T> &delegate, QQmlEngine *engine)
{
Q_ASSERT(!delegate.wasExecuted());
QtQuickPrivate::completeDeferred(object, property, &delegate, engine);
delegate.setExecuted();
}
QT_END_NAMESPACE
#endif // QQUICKDEFERREDEXECUTE_P_P_H

View File

@ -16,7 +16,6 @@ ApplicationWindow {
radius: width / 2
ContextMenu.menu: Menu {
id: contextMenu
MenuItem {
text: qsTr("Eat tomato")
}

View File

@ -16,7 +16,6 @@ ApplicationWindow {
radius: width / 2
ContextMenu.menu: Menu {
id: contextMenu
MenuItem {
text: qsTr("Eat tomato")
}

View File

@ -6,9 +6,12 @@ ApplicationWindow {
width: 600
height: 400
property bool handlerGotEvent
signal eventReceived(QtObject receiver)
contentItem.ContextMenu.menu: Menu {
objectName: "menu"
onOpened: window.eventReceived(this)
MenuItem {
text: qsTr("Eat tomato")
}
@ -21,10 +24,11 @@ ApplicationWindow {
}
TapHandler {
objectName: "tapHandler"
acceptedButtons: Qt.RightButton
// Ensure that it grabs mouse on press, as the default gesture policy results in a passive grab,
// which would allow the ContextMenu to receive the context menu event.
gesturePolicy: TapHandler.WithinBounds
onTapped: window.handlerGotEvent = true
onTapped: window.eventReceived(this)
}
}

View File

@ -0,0 +1,25 @@
import QtQuick
import QtQuick.Controls
ApplicationWindow {
width: 600
height: 400
Pane {
anchors.fill: parent
ContextMenu.menu: Menu {
id: uhOh
MenuItem {
text: qsTr("Eat tomato")
}
MenuItem {
text: qsTr("Throw tomato")
}
MenuItem {
text: qsTr("Squash tomato")
}
}
}
}

View File

@ -3,6 +3,7 @@
#include <QtGui/private/qguiapplication_p.h>
#include <QtGui/qpa/qplatformtheme.h>
#include <QtTest/qsignalspy.h>
#include <QtTest/qtest.h>
#include <QtQuick/qquickview.h>
#include <QtQuickTestUtils/private/viewtestutils_p.h>
@ -28,12 +29,16 @@ private slots:
void eventOrder();
void notAttachedToItem();
void nullMenu();
void idOnMenu();
void createOnRequested_data();
void createOnRequested();
private:
bool contextMenuTriggeredOnRelease = false;
};
tst_QQuickContextMenu::tst_QQuickContextMenu()
: QQmlDataTest(QT_QMLTEST_DATADIR)
: QQmlDataTest(QT_QMLTEST_DATADIR, FailOnWarningsPolicy::FailOnWarnings)
{
}
@ -43,6 +48,9 @@ void tst_QQuickContextMenu::initTestCase()
// Can't test native menus with QTest.
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuWindows);
contextMenuTriggeredOnRelease = QGuiApplicationPrivate::platformTheme()->themeHint(
QPlatformTheme::ContextMenuOnMouseRelease).toBool();
}
void tst_QQuickContextMenu::customContextMenu_data()
@ -69,22 +77,28 @@ void tst_QQuickContextMenu::customContextMenu()
auto *tomatoItem = window->findChild<QQuickItem *>("tomato");
QVERIFY(tomatoItem);
const bool contextMenuTriggeredOnRelease = QGuiApplicationPrivate::platformTheme()->themeHint(
QPlatformTheme::ContextMenuOnMouseRelease).toBool();
const QPoint &tomatoCenter = mapCenterToWindow(tomatoItem);
QQuickMenu *menu = window->findChild<QQuickMenu *>();
QVERIFY(menu);
QTest::mousePress(window, Qt::RightButton, Qt::NoModifier, tomatoCenter);
QTRY_COMPARE(menu->isOpened(), !contextMenuTriggeredOnRelease);
// Due to the menu property being deferred, the Menu isn't created until
// the context menu event is received, so we can't look for it before the press.
QQuickMenu *menu = window->findChild<QQuickMenu *>();
if (!contextMenuTriggeredOnRelease) {
QVERIFY(menu);
QTRY_VERIFY(menu->isOpened());
} else {
// It's triggered on press, so it shouldn't exist yet.
QVERIFY(!menu);
}
QTest::mouseRelease(window, Qt::RightButton, Qt::NoModifier, tomatoCenter);
if (contextMenuTriggeredOnRelease)
menu = window->findChild<QQuickMenu *>();
#ifdef Q_OS_WIN
if (qgetenv("QTEST_ENVIRONMENT").split(' ').contains("ci"))
QSKIP("Menu fails to open on Windows (QTBUG-132436)");
#endif
QTRY_COMPARE(menu->isOpened(), true);
QVERIFY(menu);
QTRY_VERIFY(menu->isOpened());
// Popups are positioned relative to their parent, and it should be opened at the center:
// width (100) / 2 = 50
@ -138,8 +152,10 @@ void tst_QQuickContextMenu::sharedContextMenu()
QCOMPARE(menu->itemAt(0)->property("text").toString(), "Eat really ripe tomato");
}
// We should only send the context menu event if another item higher in the stacking order
// didn't accept the mouse event.
// After 70c61b12efe9d1faf24063b63cf5a69414d45cea in qtbase, accepting a press/release will not
// prevent an item beneath the accepting item from getting a context menu event.
// This test was originally written before that, and would verify that only the handler
// got the event. Now it checks that both received events in the correct order.
void tst_QQuickContextMenu::eventOrder()
{
QQuickApplicationHelper helper(this, "deliverToHandlersBeforeContextMenu.qml");
@ -148,15 +164,26 @@ void tst_QQuickContextMenu::eventOrder()
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
QSignalSpy eventReceivedSpy(window, SIGNAL(eventReceived(QObject *)));
QVERIFY(eventReceivedSpy.isValid());
const QPoint &windowCenter = mapCenterToWindow(window->contentItem());
QTest::mouseClick(window, Qt::RightButton, Qt::NoModifier, windowCenter);
QVERIFY(window->property("handlerGotEvent").toBool());
// There shouldn't be a menu since the attached type's context event handler
// never got the event and hence the menu was never created.
auto *menu = window->findChild<QQuickMenu *>();
QEXPECT_FAIL("", "TODO: we need to fix deferred execution so that the menu "
"is created lazily before this will pass", Continue);
QVERIFY(!menu);
// First check that the menu was actually created, as this is an easier to understand
// failure message than a signal spy count mismatch.
const auto *menu = window->findChild<QQuickMenu *>();
QVERIFY(menu);
QCOMPARE(eventReceivedSpy.count(), 2);
const auto *tapHandler = window->findChild<QObject *>("tapHandler");
QVERIFY(tapHandler);
if (!contextMenuTriggeredOnRelease) {
// If the context menu is triggered on press, it will open before the handler gets the release.
QCOMPARE(eventReceivedSpy.at(0).at(0).value<QObject *>(), menu);
QCOMPARE(eventReceivedSpy.at(1).at(0).value<QObject *>(), tapHandler);
} else {
QCOMPARE(eventReceivedSpy.at(0).at(0).value<QObject *>(), tapHandler);
QCOMPARE(eventReceivedSpy.at(1).at(0).value<QObject *>(), menu);
}
}
void tst_QQuickContextMenu::notAttachedToItem()
@ -175,13 +202,29 @@ void tst_QQuickContextMenu::nullMenu()
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
// Shouldn't crash.
// Shouldn't crash or warn.
const QPoint &windowCenter = mapCenterToWindow(window->contentItem());
QTest::mouseClick(window, Qt::RightButton, Qt::NoModifier, windowCenter);
auto *menu = window->findChild<QQuickMenu *>();
QVERIFY(!menu);
}
void tst_QQuickContextMenu::idOnMenu()
{
QQuickApplicationHelper helper(this, "idOnMenu.qml");
QVERIFY2(helper.ready, helper.failureMessage());
QQuickWindow *window = helper.window;
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
// Giving the menu an id prevents deferred execution, but the menu should still work.
const QPoint &windowCenter = mapCenterToWindow(window->contentItem());
QTest::mouseClick(window, Qt::RightButton, Qt::NoModifier, windowCenter);
auto *menu = window->findChild<QQuickMenu *>();
QVERIFY(menu);
QVERIFY(menu->isOpened());
}
void tst_QQuickContextMenu::createOnRequested_data()
{
QTest::addColumn<bool>("programmaticShow");