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:
parent
b7e341ec5a
commit
5a987204a6
|
@ -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]
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -16,7 +16,6 @@ ApplicationWindow {
|
|||
radius: width / 2
|
||||
|
||||
ContextMenu.menu: Menu {
|
||||
id: contextMenu
|
||||
MenuItem {
|
||||
text: qsTr("Eat tomato")
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ ApplicationWindow {
|
|||
radius: width / 2
|
||||
|
||||
ContextMenu.menu: Menu {
|
||||
id: contextMenu
|
||||
MenuItem {
|
||||
text: qsTr("Eat tomato")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
Loading…
Reference in New Issue