1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquickcontextmenu_p.h"
5
6#include <QtCore/qpointer.h>
7#include <QtCore/qloggingcategory.h>
8#include <QtQml/qqmlcomponent.h>
9#include <QtQml/qqmlinfo.h>
10#include <QtQuick/qquickwindow.h>
11#include <QtQuick/private/qquickitem_p.h>
12#include <QtQuickTemplates2/private/qquickdeferredexecute_p_p.h>
13#include <QtQuickTemplates2/private/qquickmenu_p.h>
14
15QT_BEGIN_NAMESPACE
16
17Q_STATIC_LOGGING_CATEGORY(lcContextMenu, "qt.quick.controls.contextmenu")
18
19/*!
20 \qmltype ContextMenu
21 \brief The ContextMenu attached type provides a way to open a context menu
22 in a platform-appropriate manner.
23 \inqmlmodule QtQuick.Controls
24 \ingroup qtquickcontrols-menus
25 \since 6.9
26
27 ContextMenu can be attached to any \l {QQuickItem}{item} in order to show a context menu
28 upon a platform-specific event, such as a right click or the context menu
29 key.
30
31 \snippet qtquickcontrols-contextmenu.qml root
32
33 \section1 Sharing context menus
34
35 It's possible to share a \l Menu amongst several attached context menu
36 objects. This allows reusing a single Menu when the items that need
37 context menus have data in common. For example:
38
39 \snippet qtquickcontrols-contextmenu-shared.qml file
40
41 \section1 Performance
42
43 ContextMenu lazily creates its \c Menu only when it's requested. If it
44 wasn't for this optimization, the \c Menu would be created when the
45 containing component is being loaded, which is typically at application
46 startup.
47
48 It is recommended not to give the \c Menu assigned to ContextMenu's \l menu
49 property an id when it's defined where it's assigned. Doing so prevents
50 this optimization. For example:
51
52 \snippet qtquickcontrols-contextmenu-id.qml root
53
54 The example in the \l {Sharing context menus} section works because the
55 \c Menu is defined separately from its assignment.
56
57 \section1 Interaction with other menus
58
59 If a \c Menu is opened via e.g. a \l TapHandler or other means, ContextMenu
60 will not open at the same time. This allows legacy applications that were
61 written before ContextMenu was introduced to continue working as expected.
62*/
63
64/*!
65 \qmlsignal QtQuick.Controls::ContextMenu::requested(point position)
66
67 This signal is emitted when a context menu is requested.
68
69 If it was requested by a right mouse button click, \a position gives the
70 position of the click relative to the parent.
71
72 The example below shows how to programmatically open a context menu:
73
74 \snippet qtquickcontrols-contextmenu-onrequested.qml buttonAndMenu
75
76 If no menu is set, but this signal is connected, the context menu event
77 will be accepted and will not propagate.
78
79 \sa QContextMenuEvent::pos()
80*/
81
82class QQuickContextMenuPrivate : public QObjectPrivate
83{
84public:
85 Q_DECLARE_PUBLIC(QQuickContextMenu)
86
87 static QQuickContextMenuPrivate *get(QQuickContextMenu *attachedObject)
88 {
89 return attachedObject->d_func();
90 }
91
92 void cancelMenu();
93 void executeMenu(bool complete = false);
94
95 bool isRequestedSignalConnected();
96
97 QQuickDeferredPointer<QQuickMenu> menu;
98 bool complete = false;
99};
100
101static const QString menuPropertyName = QStringLiteral("menu");
102
103void QQuickContextMenuPrivate::cancelMenu()
104{
105 Q_Q(QQuickContextMenu);
106 quickCancelDeferred(object: q, property: menuPropertyName);
107}
108
109void QQuickContextMenuPrivate::executeMenu(bool complete)
110{
111 Q_Q(QQuickContextMenu);
112 if (menu.wasExecuted())
113 return;
114
115 QQmlEngine *engine = nullptr;
116 auto *parentItem = qobject_cast<QQuickItem *>(o: q->parent());
117 if (parentItem) {
118 engine = qmlEngine(parentItem);
119 // In most cases, the above line will work, but if it doesn't,
120 // it could be because we're attached to the contentItem of the window.
121 // In that case, we'll be created before the contentItem has a context
122 // and hence an engine. However, the window will have one, so use that.
123 if (!engine)
124 engine = qmlEngine(parentItem->window());
125 }
126
127 if (!menu || complete)
128 quickBeginAttachedDeferred(object: q, property: menuPropertyName, delegate&: menu, engine);
129 if (complete)
130 quickCompleteAttachedDeferred(object: q, property: menuPropertyName, delegate&: menu, engine);
131}
132
133bool QQuickContextMenuPrivate::isRequestedSignalConnected()
134{
135 Q_Q(QQuickContextMenu);
136 IS_SIGNAL_CONNECTED(q, QQuickContextMenu, requested, (QPointF));
137}
138
139QQuickContextMenu::QQuickContextMenu(QObject *parent)
140 : QObject(*(new QQuickContextMenuPrivate), parent)
141{
142 Q_ASSERT(parent);
143 if (parent->isQuickItemType()) {
144 auto *itemPriv = QQuickItemPrivate::get(item: static_cast<QQuickItem *>(parent));
145 Q_ASSERT(itemPriv);
146 if (QObject *oldMenu = itemPriv->setContextMenu(this))
147 qCWarning(lcContextMenu) << this << "replaced" << oldMenu << "on" << parent;
148 } else {
149 qmlWarning(me: parent) << "ContextMenu must be attached to an Item";
150 }
151}
152
153QQuickContextMenu *QQuickContextMenu::qmlAttachedProperties(QObject *object)
154{
155 return new QQuickContextMenu(object);
156}
157
158/*!
159 \qmlproperty Menu QtQuick.Controls::ContextMenu::menu
160
161 This property holds the context menu that will be opened. It can be set to
162 any \l Menu object.
163
164 \note The \l Menu assigned to this property cannot be given an id. See
165 \l {Sharing context menus} for more information.
166*/
167QQuickMenu *QQuickContextMenu::menu() const
168{
169 auto *d = const_cast<QQuickContextMenuPrivate *>(d_func());
170 if (!d->menu) {
171 qCDebug(lcContextMenu) << "creating menu via deferred execution"
172 << "- is component complete:" << d->complete;
173 d->executeMenu(complete: d->complete);
174 }
175 return d->menu;
176}
177
178void QQuickContextMenu::setMenu(QQuickMenu *menu)
179{
180 Q_D(QQuickContextMenu);
181 if (!parent()->isQuickItemType())
182 return;
183
184 if (menu == d->menu)
185 return;
186
187 if (!d->menu.isExecuting())
188 d->cancelMenu();
189
190 d->menu = menu;
191
192 if (!d->menu.isExecuting())
193 emit menuChanged();
194}
195
196void QQuickContextMenu::classBegin()
197{
198}
199
200void QQuickContextMenu::componentComplete()
201{
202 Q_D(QQuickContextMenu);
203 d->complete = true;
204}
205
206bool QQuickContextMenu::event(QEvent *event)
207{
208 Q_D(QQuickContextMenu);
209 switch (event->type()) {
210 case QEvent::ContextMenu: {
211 qCDebug(lcContextMenu) << this << "handling" << event << "on behalf of" << parent();
212
213 auto *attacheeItem = qobject_cast<QQuickItem *>(o: parent());
214 Q_ASSERT(attacheeItem);
215 const auto *contextMenuEvent = static_cast<QContextMenuEvent *>(event);
216 const QPoint posRelativeToParent(attacheeItem->mapFromScene(point: contextMenuEvent->pos()).toPoint());
217
218 const bool isRequestedSignalConnected = d->isRequestedSignalConnected();
219 if (isRequestedSignalConnected)
220 Q_EMIT requested(position: posRelativeToParent);
221
222 auto *menu = this->menu();
223 if (!menu) {
224 if (isRequestedSignalConnected) {
225 qCDebug(lcContextMenu) << this << "no menu instance but accepting event anyway"
226 << "since requested signal has connections";
227 event->accept();
228 return true;
229 }
230
231 // No menu set and requested isn't connected; let the event propagate
232 // onwards and do nothing.
233 return QObject::event(event);
234 }
235
236 menu->setParentItem(attacheeItem);
237
238 qCDebug(lcContextMenu) << this << "showing" << menu << "at" << posRelativeToParent;
239 menu->popup(position: posRelativeToParent);
240 event->accept();
241 return true;
242 }
243 default:
244 break;
245 }
246 return QObject::event(event);
247}
248
249QT_END_NAMESPACE
250
251#include "moc_qquickcontextmenu_p.cpp"
252

source code of qtdeclarative/src/quicktemplates/qquickcontextmenu.cpp