aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMitch Curtis <[email protected]>2023-11-21 11:12:31 +0800
committerMitch Curtis <[email protected]>2024-01-16 18:09:10 +0800
commitd84556e6bcc91690e5fccd1a909707698b6be56c (patch)
tree9047a4e927eed8ca643680a8d0c02cd4ec1a8cd3
parent8ba21bb39fdb92de889c3ba5f3bc10428576a5f3 (diff)
Add beginnings of native Menu backend
This patch gets the basic functionality and initial API in place so that work on MenuBar can begin. Task-number: QTBUG-69558 Change-Id: I94df848f771d38cd1cabb964b695d383f66240f2 Reviewed-by: Richard Moe Gustavsen <[email protected]>
-rw-r--r--src/quicktemplates/CMakeLists.txt6
-rw-r--r--src/quicktemplates/qquickmenu.cpp454
-rw-r--r--src/quicktemplates/qquickmenu_p.h11
-rw-r--r--src/quicktemplates/qquickmenu_p_p.h22
-rw-r--r--src/quicktemplates/qquicknativeicon.cpp50
-rw-r--r--src/quicktemplates/qquicknativeicon_p.h57
-rw-r--r--src/quicktemplates/qquicknativeiconloader.cpp68
-rw-r--r--src/quicktemplates/qquicknativeiconloader_p.h54
-rw-r--r--src/quicktemplates/qquicknativemenuitem.cpp207
-rw-r--r--src/quicktemplates/qquicknativemenuitem_p.h64
-rw-r--r--tests/auto/quickcontrols/CMakeLists.txt6
-rw-r--r--tests/auto/quickcontrols/nativemenus/CMakeLists.txt43
-rw-r--r--tests/auto/quickcontrols/nativemenus/data/emptyMenu.qml50
-rw-r--r--tests/auto/quickcontrols/nativemenus/data/staticActionsAndSubmenus.qml56
-rw-r--r--tests/auto/quickcontrols/nativemenus/tst_nativemenus.cpp181
-rw-r--r--tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp2
16 files changed, 1297 insertions, 34 deletions
diff --git a/src/quicktemplates/CMakeLists.txt b/src/quicktemplates/CMakeLists.txt
index 067d3c1134..49e6cad841 100644
--- a/src/quicktemplates/CMakeLists.txt
+++ b/src/quicktemplates/CMakeLists.txt
@@ -46,6 +46,12 @@ qt_internal_add_qml_module(QuickTemplates2
qquicklabel.cpp qquicklabel_p.h
qquicklabel_p_p.h
qquickmenuseparator.cpp qquickmenuseparator_p.h
+ qquicknativeicon_p.h
+ qquicknativeicon.cpp
+ qquicknativeiconloader_p.h
+ qquicknativeiconloader.cpp
+ qquicknativemenuitem_p.h
+ qquicknativemenuitem.cpp
qquickoverlay.cpp qquickoverlay_p.h
qquickoverlay_p_p.h
qquickpage.cpp qquickpage_p.h
diff --git a/src/quicktemplates/qquickmenu.cpp b/src/quicktemplates/qquickmenu.cpp
index e0f6337b60..47b6d05331 100644
--- a/src/quicktemplates/qquickmenu.cpp
+++ b/src/quicktemplates/qquickmenu.cpp
@@ -9,16 +9,20 @@
#include "qquickmenubaritem_p.h"
#include "qquickmenubar_p.h"
#endif
+#include "qquicknativemenuitem_p.h"
#include "qquickpopupitem_p_p.h"
#include "qquickpopuppositioner_p_p.h"
#include "qquickaction_p.h"
+#include <QtCore/qloggingcategory.h>
#include <QtGui/qevent.h>
#include <QtGui/qcursor.h>
#if QT_CONFIG(shortcut)
#include <QtGui/qkeysequence.h>
#endif
#include <QtGui/qpa/qplatformintegration.h>
+#include <QtGui/qpa/qplatformtheme.h>
+#include <QtGui/private/qhighdpiscaling_p.h>
#include <QtGui/private/qguiapplication_p.h>
#include <QtQml/qqmlcontext.h>
#include <QtQml/qqmlcomponent.h>
@@ -30,10 +34,13 @@
#include <QtQuick/private/qquickitem_p.h>
#include <QtQuick/private/qquickitemchangelistener_p.h>
#include <QtQuick/private/qquickevents_p_p.h>
+#include <QtQuick/private/qquickrendercontrol_p.h>
#include <QtQuick/private/qquickwindow_p.h>
QT_BEGIN_NAMESPACE
+Q_LOGGING_CATEGORY(lcNativeMenu, "qt.quick.controls.nativemenu")
+
// copied from qfusionstyle.cpp
static const int SUBMENU_DELAY = 225;
@@ -228,6 +235,158 @@ void QQuickMenuPrivate::init()
{
Q_Q(QQuickMenu);
contentModel = new QQmlObjectModel(q);
+
+ // TODO: use an env var until we get Qt::AA_DontUseNativeMenu
+ requestNative = qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_USE_NATIVE_MENUS");
+}
+
+bool QQuickMenuPrivate::usingNativeMenu()
+{
+ if (requestNative && !triedToCreateNativeMenu)
+ createNativeMenu();
+
+ return nativeHandle.get();
+}
+
+bool QQuickMenuPrivate::createNativeMenu()
+{
+ Q_ASSERT(!nativeHandle);
+ Q_Q(QQuickMenu);
+ qCDebug(lcNativeMenu) << "createNativeMenu called on" << q;
+
+ if (!nativeHandle) {
+ QPlatformMenu *parentMenuHandle(parentMenu ? get(parentMenu)->nativeHandle.get() : nullptr);
+ if (parentMenu && parentMenuHandle) {
+ qCDebug(lcNativeMenu) << "- creating native sub-menu";
+ nativeHandle.reset(parentMenuHandle->createSubMenu());
+ } else {
+ qCDebug(lcNativeMenu) << "- creating native menu";
+ nativeHandle.reset(QGuiApplicationPrivate::platformTheme()->createPlatformMenu());
+ }
+ }
+
+ triedToCreateNativeMenu = true;
+
+ if (!nativeHandle)
+ return false;
+
+ q->connect(nativeHandle.get(), &QPlatformMenu::aboutToShow, q, &QQuickPopup::aboutToShow);
+ q->connect(nativeHandle.get(), &QPlatformMenu::aboutToHide, q, [this](){
+ qCDebug(lcNativeMenu) << "QPlatformMenu::aboutToHide called; about to call setVisible(false) on Menu";
+ q_func()->setVisible(false);
+ });
+
+ for (QQuickNativeMenuItem *item : std::as_const(nativeItems))
+ nativeHandle->insertMenuItem(item->create(), nullptr);
+
+ // TODO: we call setMenu in QQuickNativeMenuItem::create()
+ // for sub-menus. do we also need to do it here?
+// if (m_menuItem) {
+// if (QPlatformMenuItem *handle = m_menuItem->create())
+// handle->setMenu(d->handle);
+// }
+
+ return true;
+}
+
+QString nativeMenuItemListToString(const QList<QQuickNativeMenuItem *> &nativeItems)
+{
+ QString str;
+ QTextStream debug(&str);
+ for (const auto *nativeItem : nativeItems)
+ debug << nativeItem->debugText() << ", ";
+ // Remove trailing space and comma.
+ if (!nativeItems.isEmpty())
+ str.chop(2);
+ return str;
+}
+
+void QQuickMenuPrivate::syncWithNativeMenu()
+{
+ Q_Q(QQuickMenu);
+ qCDebug(lcNativeMenu) << "syncWithNativeMenu called on" << q
+ << "complete:" << complete << "visible:" << visible;
+ if (!complete || !usingNativeMenu())
+ return;
+
+ // TODO: call this function when any of the variables below change
+
+ nativeHandle->setText(title);
+ nativeHandle->setEnabled(q->isEnabled());
+ nativeHandle->setVisible(visible);
+ nativeHandle->setMinimumWidth(q->implicitWidth());
+// nativeHandle->setMenuType(m_type);
+ nativeHandle->setFont(q->font());
+
+// if (m_menuBar && m_menuBar->handle())
+// m_menuBar->handle()->syncMenu(handle);
+//#if QT_CONFIG(systemtrayicon)
+// else if (m_systemTrayIcon && m_systemTrayIcon->handle())
+// m_systemTrayIcon->handle()->updateMenu(handle);
+//#endif
+
+ qCDebug(lcNativeMenu) << "syncing" << nativeItems.size() << "item(s)...";
+ for (QQuickNativeMenuItem *item : std::as_const(nativeItems)) {
+ qCDebug(lcNativeMenu) << "- syncing" << item << "action" << item->action()
+ << "sub-menu" << item->subMenu() << item->debugText();
+ item->sync();
+ }
+}
+
+void QQuickMenuPrivate::destroyNativeMenu()
+{
+ if (!nativeHandle)
+ return;
+
+ // Ensure that all submenus are unparented before we are destroyed,
+ // so that they don't try to access a destroyed menu.
+ for (QQuickNativeMenuItem *item : std::as_const(nativeItems)) {
+ if (QQuickMenu *subMenu = item->subMenu())
+ QQuickMenuPrivate::get(subMenu)->destroyNativeMenu();
+ item->clearSubMenu();
+ }
+
+ nativeHandle.reset();
+}
+
+static QWindow *effectiveWindow(QWindow *window, QPoint *offset)
+{
+ QQuickWindow *quickWindow = qobject_cast<QQuickWindow *>(window);
+ if (quickWindow) {
+ QWindow *renderWindow = QQuickRenderControl::renderWindowFor(quickWindow, offset);
+ if (renderWindow)
+ return renderWindow;
+ }
+ return window;
+}
+
+void QQuickMenuPrivate::setNativeMenuVisible(bool visible)
+{
+ Q_Q(QQuickMenu);
+ qCDebug(lcNativeMenu) << "setNativeMenuVisible called with visible" << visible;
+ if (visible)
+ emit q->aboutToShow();
+ else
+ emit q->aboutToHide();
+
+ this->visible = visible;
+ syncWithNativeMenu();
+
+ QPoint offset;
+ QWindow *window = effectiveWindow(qGuiApp->topLevelWindows().first(), &offset);
+
+ QRect targetRect;
+#if QT_CONFIG(cursor)
+ QPoint pos = QCursor::pos();
+ if (window)
+ pos = window->mapFromGlobal(pos);
+ targetRect.moveTo(pos);
+#endif
+ if (visible)
+ nativeHandle->showPopup(window, QHighDpi::toNativePixels(targetRect, window),
+ /*menuItem ? menuItem->handle() : */nullptr);
+ else
+ nativeHandle->dismiss();
}
QQuickItem *QQuickMenuPrivate::itemAt(int index) const
@@ -283,6 +442,146 @@ void QQuickMenuPrivate::removeItem(int index, QQuickItem *item)
}
}
+int QQuickMenuPrivate::indexOfActionInNativeItems(QQuickAction *action) const
+{
+ const auto existingItemIt = std::find_if(
+ nativeItems.constBegin(), nativeItems.constEnd(), [action](QQuickNativeMenuItem *item) {
+ return item->action() == action;
+ });
+ const int index = existingItemIt != nativeItems.constEnd()
+ ? std::distance(nativeItems.constBegin(), existingItemIt) : -1;
+ return index;
+}
+
+int QQuickMenuPrivate::indexOfMenuInNativeItems(QQuickMenu *menu) const
+{
+ const auto existingItemIt = std::find_if(
+ nativeItems.constBegin(), nativeItems.constEnd(), [menu](QQuickNativeMenuItem *item) {
+ return item->subMenu() == menu;
+ });
+ const int index = existingItemIt != nativeItems.constEnd()
+ ? std::distance(nativeItems.constBegin(), existingItemIt) : -1;
+ return index;
+}
+
+void QQuickMenuPrivate::insertNativeItem(int index, QQuickAction *action)
+{
+ Q_Q(QQuickMenu);
+ Q_ASSERT(usingNativeMenu());
+ qCDebug(lcNativeMenu) << "insertNativeItem called on" << q << "with action" << action->text();
+
+ const int count = nativeItems.count();
+ if (index < 0 || index > count)
+ index = count;
+
+ const int oldIndex = indexOfActionInNativeItems(action);
+ if (oldIndex != -1) {
+ // The action already exists; move it if necessary.
+ if (oldIndex < index)
+ --index;
+ if (oldIndex != index) {
+ qCDebug(lcNativeMenu).nospace() << "- already exists at a different index " << oldIndex
+ << "; moving to" << index;
+ nativeItems.move(oldIndex, index);
+ } else {
+ qCDebug(lcNativeMenu).nospace() << "- already exists at the same index";
+ }
+ } else {
+ auto *nativeMenuItem = new QQuickNativeMenuItem(q, action);
+ nativeItems.insert(index, nativeMenuItem);
+
+ if (nativeMenuItem->create()) {
+ QQuickNativeMenuItem *before = nativeItems.value(index + 1);
+ nativeHandle->insertMenuItem(nativeMenuItem->handle(), before ? before->create() : nullptr);
+ qCDebug(lcNativeMenu) << "- inserted native menu item at index" << index
+ << "before" << (before ? before->debugText() : QStringLiteral("null"));
+ }
+ }
+
+ qCDebug(lcNativeMenu) << "- nativeItems now contains the following items:"
+ << nativeMenuItemListToString(nativeItems);
+
+ contentData.insert(index, action);
+
+ syncWithNativeMenu();
+}
+
+void QQuickMenuPrivate::insertNativeItem(int index, QQuickMenu *menu)
+{
+ Q_Q(QQuickMenu);
+ Q_ASSERT(usingNativeMenu());
+ qCDebug(lcNativeMenu) << "insertNativeItem called on" << q << "with menu" << menu->title();
+
+ const int count = nativeItems.count();
+ if (index < 0 || index > count)
+ index = count;
+
+ const int oldIndex = indexOfMenuInNativeItems(menu);
+ if (oldIndex != -1) {
+ // The menu already exists; move it if necessary.
+ if (oldIndex < index)
+ --index;
+ if (oldIndex != index) {
+ qCDebug(lcNativeMenu).nospace() << "- already exists at a different index " << oldIndex
+ << "; moving to" << index;
+ nativeItems.move(oldIndex, index);
+ } else {
+ qCDebug(lcNativeMenu).nospace() << "- already exists at the same index";
+ }
+ } else {
+ auto *nativeMenuItem = new QQuickNativeMenuItem(q, menu);
+ nativeItems.insert(index, nativeMenuItem);
+
+ if (nativeMenuItem->create()) {
+ QQuickNativeMenuItem *before = nativeItems.value(index + 1);
+ nativeHandle->insertMenuItem(nativeMenuItem->handle(), before ? before->create() : nullptr);
+ qCDebug(lcNativeMenu) << "- inserted native menu item at index" << index
+ << "before" << (before ? before->debugText() : QStringLiteral("null"));
+ }
+ }
+
+ qCDebug(lcNativeMenu) << "- nativeItems now contains the following items:"
+ << nativeMenuItemListToString(nativeItems);
+
+ contentData.insert(index, menu);
+
+ syncWithNativeMenu();
+}
+
+void QQuickMenuPrivate::removeNativeItem(QQuickAction *action)
+{
+ Q_ASSERT(usingNativeMenu());
+
+ const int actionIndex = indexOfActionInNativeItems(action);
+ if (actionIndex == -1)
+ return;
+
+ contentData.removeAt(actionIndex);
+ QQuickNativeMenuItem *nativeItem = nativeItems.takeAt(actionIndex);
+ nativeHandle->removeMenuItem(nativeItem->handle());
+ nativeItem->handle()->setMenu(nullptr);
+ nativeItem->deleteLater();
+ action->deleteLater();
+ syncWithNativeMenu();
+}
+
+void QQuickMenuPrivate::removeNativeItem(QQuickMenu *menu)
+{
+ Q_ASSERT(usingNativeMenu());
+
+ const int menuIndex = indexOfMenuInNativeItems(menu);
+ if (menuIndex == -1)
+ return;
+
+ contentData.removeAt(menuIndex);
+ QQuickNativeMenuItem *nativeItem = nativeItems.takeAt(menuIndex);
+ nativeHandle->removeMenuItem(nativeItem->handle());
+ nativeItem->handle()->setMenu(nullptr);
+ nativeItem->deleteLater();
+ menu->deleteLater();
+ syncWithNativeMenu();
+}
+
QQuickItem *QQuickMenuPrivate::beginCreateItem()
{
Q_Q(QQuickMenu);
@@ -688,10 +987,23 @@ void QQuickMenuPrivate::contentData_append(QQmlListProperty<QObject> *prop, QObj
QQuickItem *item = qobject_cast<QQuickItem *>(obj);
if (!item) {
- if (QQuickAction *action = qobject_cast<QQuickAction *>(obj))
- item = p->createItem(action);
- else if (QQuickMenu *menu = qobject_cast<QQuickMenu *>(obj))
- item = p->createItem(menu);
+ // We need to know if the native menu was able to be created
+ // before we start trying to add items.
+ if (p->usingNativeMenu()) {
+ if (QQuickAction *action = qobject_cast<QQuickAction *>(obj)) {
+ qCDebug(lcNativeMenu) << "contentData_append called on" << q << "with action" << action->text();
+ p->insertNativeItem(p->nativeItems.count(), action);
+ } else if (QQuickMenu *menu = qobject_cast<QQuickMenu *>(obj)) {
+ qCDebug(lcNativeMenu) << "contentData_append called on" << q << "with menu" << menu->title();
+ p->insertNativeItem(p->nativeItems.count(), menu);
+ }
+ return;
+ } else {
+ if (QQuickAction *action = qobject_cast<QQuickAction *>(obj))
+ item = p->createItem(action);
+ else if (QQuickMenu *menu = qobject_cast<QQuickMenu *>(obj))
+ item = p->createItem(menu);
+ }
}
if (item) {
@@ -755,6 +1067,8 @@ QQuickMenu::~QQuickMenu()
for (QQuickItem *child : std::as_const(children))
QQuickItemPrivate::get(child)->removeItemChangeListener(d, QQuickItemPrivate::SiblingOrder);
}
+
+ d->destroyNativeMenu();
}
/*!
@@ -873,11 +1187,18 @@ QQuickItem *QQuickMenu::takeItem(int index)
QQuickMenu *QQuickMenu::menuAt(int index) const
{
Q_D(const QQuickMenu);
- QQuickMenuItem *item = qobject_cast<QQuickMenuItem *>(d->itemAt(index));
- if (!item)
- return nullptr;
+ if (!const_cast<QQuickMenuPrivate *>(d)->usingNativeMenu()) {
+ QQuickMenuItem *item = qobject_cast<QQuickMenuItem *>(d->itemAt(index));
+ if (!item)
+ return nullptr;
- return item->subMenu();
+ return item->subMenu();
+ } else {
+ if (index < 0 || index >= d->nativeItems.size())
+ return nullptr;
+
+ return d->nativeItems.at(index)->subMenu();
+ }
}
/*!
@@ -889,7 +1210,10 @@ QQuickMenu *QQuickMenu::menuAt(int index) const
void QQuickMenu::addMenu(QQuickMenu *menu)
{
Q_D(QQuickMenu);
- insertMenu(d->contentModel->count(), menu);
+ if (!d->usingNativeMenu())
+ insertMenu(d->contentModel->count(), menu);
+ else
+ insertMenu(d->nativeItems.count(), menu);
}
/*!
@@ -904,7 +1228,10 @@ void QQuickMenu::insertMenu(int index, QQuickMenu *menu)
if (!menu)
return;
- insertItem(index, d->createItem(menu));
+ if (!d->usingNativeMenu())
+ insertItem(index, d->createItem(menu));
+ else
+ d->insertNativeItem(index, menu);
}
/*!
@@ -919,17 +1246,21 @@ void QQuickMenu::removeMenu(QQuickMenu *menu)
if (!menu)
return;
- const int count = d->contentModel->count();
- for (int i = 0; i < count; ++i) {
- QQuickMenuItem *item = qobject_cast<QQuickMenuItem *>(d->itemAt(i));
- if (!item || item->subMenu() != menu)
- continue;
+ if (!d->usingNativeMenu()) {
+ const int count = d->contentModel->count();
+ for (int i = 0; i < count; ++i) {
+ QQuickMenuItem *item = qobject_cast<QQuickMenuItem *>(d->itemAt(i));
+ if (!item || item->subMenu() != menu)
+ continue;
- removeItem(item);
- break;
- }
+ removeItem(item);
+ break;
+ }
- menu->deleteLater();
+ menu->deleteLater();
+ } else {
+ d->removeNativeItem(menu);
+ }
}
/*!
@@ -966,11 +1297,18 @@ QQuickMenu *QQuickMenu::takeMenu(int index)
QQuickAction *QQuickMenu::actionAt(int index) const
{
Q_D(const QQuickMenu);
- QQuickAbstractButton *item = qobject_cast<QQuickAbstractButton *>(d->itemAt(index));
- if (!item)
- return nullptr;
+ if (!const_cast<QQuickMenuPrivate *>(d)->usingNativeMenu()) {
+ QQuickAbstractButton *item = qobject_cast<QQuickAbstractButton *>(d->itemAt(index));
+ if (!item)
+ return nullptr;
+
+ return item->action();
+ } else {
+ if (index < 0 || index >= d->nativeItems.size())
+ return nullptr;
- return item->action();
+ return d->nativeItems.at(index)->action();
+ }
}
/*!
@@ -982,7 +1320,10 @@ QQuickAction *QQuickMenu::actionAt(int index) const
void QQuickMenu::addAction(QQuickAction *action)
{
Q_D(QQuickMenu);
- insertAction(d->contentModel->count(), action);
+ if (!d->usingNativeMenu())
+ insertAction(d->contentModel->count(), action);
+ else
+ d->insertNativeItem(d->nativeItems.count(), action);
}
/*!
@@ -997,7 +1338,10 @@ void QQuickMenu::insertAction(int index, QQuickAction *action)
if (!action)
return;
- insertItem(index, d->createItem(action));
+ if (!d->usingNativeMenu())
+ insertItem(index, d->createItem(action));
+ else
+ d->insertNativeItem(index, action);
}
/*!
@@ -1012,17 +1356,21 @@ void QQuickMenu::removeAction(QQuickAction *action)
if (!action)
return;
- const int count = d->contentModel->count();
- for (int i = 0; i < count; ++i) {
- QQuickMenuItem *item = qobject_cast<QQuickMenuItem *>(d->itemAt(i));
- if (!item || item->action() != action)
- continue;
+ if (!d->usingNativeMenu()) {
+ const int count = d->contentModel->count();
+ for (int i = 0; i < count; ++i) {
+ QQuickMenuItem *item = qobject_cast<QQuickMenuItem *>(d->itemAt(i));
+ if (!item || item->action() != action)
+ continue;
- removeItem(item);
- break;
- }
+ removeItem(item);
+ break;
+ }
- action->deleteLater();
+ action->deleteLater();
+ } else {
+ d->removeNativeItem(action);
+ }
}
/*!
@@ -1049,6 +1397,22 @@ QQuickAction *QQuickMenu::takeAction(int index)
return action;
}
+void QQuickMenu::setVisible(bool visible)
+{
+ Q_D(QQuickMenu);
+ if (visible == d->visible)
+ return;
+
+ if (d->usingNativeMenu()) {
+ d->setNativeMenuVisible(visible);
+ return;
+ }
+
+ // Either the native menu wasn't wanted, or it couldn't be created;
+ // show the non-native menu.
+ QQuickPopup::setVisible(visible);
+}
+
/*!
\qmlproperty model QtQuick.Controls::Menu::contentModel
\readonly
@@ -1198,6 +1562,28 @@ void QQuickMenu::resetCascade()
setCascade(shouldCascade());
}
+bool QQuickMenu::requestNative() const
+{
+ Q_D(const QQuickMenu);
+ return d->requestNative;
+}
+
+void QQuickMenu::setRequestNative(bool native)
+{
+ Q_D(QQuickMenu);
+ if (d->requestNative == native)
+ return;
+
+ d->requestNative = native;
+ // TODO: do stuff
+ emit requestNativeChanged();
+}
+
+void QQuickMenu::resetRequestNative()
+{
+ setRequestNative(true);
+}
+
/*!
\since QtQuick.Controls 2.3 (Qt 5.10)
\qmlproperty real QtQuick.Controls::Menu::overlap
diff --git a/src/quicktemplates/qquickmenu_p.h b/src/quicktemplates/qquickmenu_p.h
index 3ff51bff0c..76412014df 100644
--- a/src/quicktemplates/qquickmenu_p.h
+++ b/src/quicktemplates/qquickmenu_p.h
@@ -45,6 +45,9 @@ class Q_QUICKTEMPLATES2_PRIVATE_EXPORT QQuickMenu : public QQuickPopup
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged FINAL REVISION(2, 3))
// 6.5 (Qt 6.5)
Q_PROPERTY(QQuickIcon icon READ icon WRITE setIcon NOTIFY iconChanged FINAL REVISION(6, 5))
+ // 6.7
+ Q_PROPERTY(bool requestNative READ requestNative WRITE setRequestNative RESET resetRequestNative
+ NOTIFY requestNativeChanged FINAL REVISION(6, 7))
Q_CLASSINFO("DefaultProperty", "contentData")
QML_NAMED_ELEMENT(Menu)
QML_ADDED_IN_VERSION(2, 0)
@@ -72,6 +75,10 @@ public:
void setCascade(bool cascade);
void resetCascade();
+ bool requestNative() const;
+ void setRequestNative(bool native);
+ void resetRequestNative();
+
qreal overlap() const;
void setOverlap(qreal overlap);
@@ -97,6 +104,8 @@ public:
Q_REVISION(2, 3) Q_INVOKABLE void removeAction(QQuickAction *action);
Q_REVISION(2, 3) Q_INVOKABLE QQuickAction *takeAction(int index);
+ void setVisible(bool visible) override;
+
void popup(QQuickItem *menuItem = nullptr);
void popup(const QPointF &pos, QQuickItem *menuItem = nullptr);
@@ -119,6 +128,8 @@ Q_SIGNALS:
Q_REVISION(2, 3) void currentIndexChanged();
// 6.5 (Qt 6.5)
Q_REVISION(6, 5) void iconChanged(const QQuickIcon &icon);
+ // 6.7
+ Q_REVISION(6, 7) void requestNativeChanged();
protected:
void timerEvent(QTimerEvent *event) override;
diff --git a/src/quicktemplates/qquickmenu_p_p.h b/src/quicktemplates/qquickmenu_p_p.h
index 509614d2d1..cbad7ac597 100644
--- a/src/quicktemplates/qquickmenu_p_p.h
+++ b/src/quicktemplates/qquickmenu_p_p.h
@@ -18,6 +18,8 @@
#include <QtCore/qlist.h>
#include <QtCore/qpointer.h>
+#include <QtGui/qpa/qplatformmenu.h>
+
#include <QtQuickTemplates2/private/qquickmenu_p.h>
#include <QtQuickTemplates2/private/qquickpopup_p_p.h>
@@ -27,6 +29,7 @@ class QQuickAction;
class QQmlComponent;
class QQmlObjectModel;
class QQuickMenuItem;
+class QQuickNativeMenuItem;
class Q_QUICKTEMPLATES2_PRIVATE_EXPORT QQuickMenuPrivate : public QQuickPopupPrivate
{
@@ -42,11 +45,24 @@ public:
void init();
+ bool usingNativeMenu();
+ bool createNativeMenu();
+ void syncWithNativeMenu();
+ void destroyNativeMenu();
+ void setNativeMenuVisible(bool visible);
+
QQuickItem *itemAt(int index) const;
void insertItem(int index, QQuickItem *item);
void moveItem(int from, int to);
void removeItem(int index, QQuickItem *item);
+ int indexOfActionInNativeItems(QQuickAction *action) const;
+ int indexOfMenuInNativeItems(QQuickMenu *menu) const;
+ void insertNativeItem(int index, QQuickAction *action);
+ void insertNativeItem(int index, QQuickMenu *menu);
+ void removeNativeItem(QQuickAction *action);
+ void removeNativeItem(QQuickMenu *menu);
+
QQuickItem *beginCreateItem();
void completeCreateItem();
@@ -94,6 +110,8 @@ public:
QPalette defaultPalette() const override;
bool cascade = false;
+ bool requestNative = false;
+ bool triedToCreateNativeMenu = false;
int hoverTimer = 0;
int currentIndex = -1;
qreal overlap = 0;
@@ -105,6 +123,10 @@ public:
QQmlComponent *delegate = nullptr;
QString title;
QQuickIcon icon;
+
+ // For native menu support.
+ std::unique_ptr<QPlatformMenu> nativeHandle = nullptr;
+ QList<QQuickNativeMenuItem *> nativeItems;
};
QT_END_NAMESPACE
diff --git a/src/quicktemplates/qquicknativeicon.cpp b/src/quicktemplates/qquicknativeicon.cpp
new file mode 100644
index 0000000000..dfc8a4cc7e
--- /dev/null
+++ b/src/quicktemplates/qquicknativeicon.cpp
@@ -0,0 +1,50 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qquicknativeicon_p.h"
+
+QT_BEGIN_NAMESPACE
+
+QUrl QQuickNativeIcon::source() const
+{
+ return m_source;
+}
+
+void QQuickNativeIcon::setSource(const QUrl& source)
+{
+ m_source = source;
+}
+
+QString QQuickNativeIcon::name() const
+{
+ return m_name;
+}
+
+void QQuickNativeIcon::setName(const QString& name)
+{
+ m_name = name;
+}
+
+bool QQuickNativeIcon::isMask() const
+{
+ return m_mask;
+}
+
+void QQuickNativeIcon::setMask(bool mask)
+{
+ m_mask = mask;
+}
+
+bool QQuickNativeIcon::operator==(const QQuickNativeIcon &other) const
+{
+ return m_source == other.m_source && m_name == other.m_name && m_mask == other.m_mask;
+}
+
+bool QQuickNativeIcon::operator!=(const QQuickNativeIcon &other) const
+{
+ return !(*this == other);
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qquicknativeicon_p.cpp"
diff --git a/src/quicktemplates/qquicknativeicon_p.h b/src/quicktemplates/qquicknativeicon_p.h
new file mode 100644
index 0000000000..db0625954a
--- /dev/null
+++ b/src/quicktemplates/qquicknativeicon_p.h
@@ -0,0 +1,57 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQUICKNATIVEICON_P_H
+#define QQUICKNATIVEICON_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtCore/qurl.h>
+#include <QtCore/qstring.h>
+
+#include <QtQml/qqmlengine.h>
+#include <QtCore/private/qglobal_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QObject;
+
+class QQuickNativeIcon
+{
+ Q_GADGET
+ QML_ANONYMOUS
+ Q_PROPERTY(QUrl source READ source WRITE setSource FINAL)
+ Q_PROPERTY(QString name READ name WRITE setName FINAL)
+ Q_PROPERTY(bool mask READ isMask WRITE setMask FINAL)
+
+public:
+ QUrl source() const;
+ void setSource(const QUrl &source);
+
+ QString name() const;
+ void setName(const QString &name);
+
+ bool isMask() const;
+ void setMask(bool mask);
+
+ bool operator==(const QQuickNativeIcon &other) const;
+ bool operator!=(const QQuickNativeIcon &other) const;
+
+private:
+ bool m_mask = false;
+ QUrl m_source;
+ QString m_name;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQUICKNATIVEICON_P_H
diff --git a/src/quicktemplates/qquicknativeiconloader.cpp b/src/quicktemplates/qquicknativeiconloader.cpp
new file mode 100644
index 0000000000..fd73af697c
--- /dev/null
+++ b/src/quicktemplates/qquicknativeiconloader.cpp
@@ -0,0 +1,68 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qquicknativeiconloader_p.h"
+
+#include <QtCore/qobject.h>
+#include <QtCore/qmetaobject.h>
+#include <QtQml/qqml.h>
+
+QT_BEGIN_NAMESPACE
+
+QQuickNativeIconLoader::QQuickNativeIconLoader(int slot, QObject *parent)
+ : m_parent(parent),
+ m_slot(slot),
+ m_enabled(false)
+{
+ Q_ASSERT(slot != -1 && parent);
+}
+
+bool QQuickNativeIconLoader::isEnabled() const
+{
+ return m_enabled;
+}
+
+void QQuickNativeIconLoader::setEnabled(bool enabled)
+{
+ m_enabled = enabled;
+ if (m_enabled)
+ loadIcon();
+}
+
+QIcon QQuickNativeIconLoader::toQIcon() const
+{
+ QIcon fallback = QPixmap::fromImage(image());
+ QIcon icon = QIcon::fromTheme(m_icon.name(), fallback);
+ icon.setIsMask(m_icon.isMask());
+ return icon;
+}
+
+QQuickNativeIcon QQuickNativeIconLoader::icon() const
+{
+ return m_icon;
+}
+
+void QQuickNativeIconLoader::setIcon(const QQuickNativeIcon &icon)
+{
+ m_icon = icon;
+ if (m_enabled)
+ loadIcon();
+}
+
+void QQuickNativeIconLoader::loadIcon()
+{
+ if (m_icon.source().isEmpty()) {
+ clear(m_parent);
+ } else {
+ load(qmlEngine(m_parent), m_icon.source());
+ if (m_slot != -1 && isLoading()) {
+ connectFinished(m_parent, m_slot);
+ m_slot = -1;
+ }
+ }
+
+ if (!isLoading())
+ m_parent->metaObject()->method(m_slot).invoke(m_parent);
+}
+
+QT_END_NAMESPACE
diff --git a/src/quicktemplates/qquicknativeiconloader_p.h b/src/quicktemplates/qquicknativeiconloader_p.h
new file mode 100644
index 0000000000..3aa896da50
--- /dev/null
+++ b/src/quicktemplates/qquicknativeiconloader_p.h
@@ -0,0 +1,54 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQUICKNATIVEICONLOADER_P_H
+#define QQUICKNATIVEICONLOADER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtCore/qurl.h>
+#include <QtCore/qstring.h>
+#include <QtGui/qicon.h>
+#include <QtQuick/private/qquickpixmap_p.h>
+
+#include "qquicknativeicon_p.h"
+
+QT_BEGIN_NAMESPACE
+
+class QObject;
+
+class QQuickNativeIconLoader : public QQuickPixmap
+{
+public:
+ QQuickNativeIconLoader(int slot, QObject *parent);
+
+ bool isEnabled() const;
+ void setEnabled(bool enabled);
+
+ QIcon toQIcon() const;
+
+ // TODO: this should probably be QQuickIcon or we should have a pointer to the action
+ QQuickNativeIcon icon() const;
+ void setIcon(const QQuickNativeIcon &icon);
+
+private:
+ void loadIcon();
+
+ QObject *m_parent;
+ int m_slot;
+ bool m_enabled;
+ QQuickNativeIcon m_icon;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQUICKNATIVEICONLOADER_P_H
diff --git a/src/quicktemplates/qquicknativemenuitem.cpp b/src/quicktemplates/qquicknativemenuitem.cpp
new file mode 100644
index 0000000000..85a60a961f
--- /dev/null
+++ b/src/quicktemplates/qquicknativemenuitem.cpp
@@ -0,0 +1,207 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qquicknativemenuitem_p.h"
+
+#include <QtCore/qloggingcategory.h>
+//#include <QtGui/qicon.h>
+//#if QT_CONFIG(shortcut)
+//#include <QtGui/qkeysequence.h>
+//#endif
+#include <QtGui/qpa/qplatformmenu.h>
+#include <QtGui/qpa/qplatformtheme.h>
+#include <QtGui/private/qguiapplication_p.h>
+//#include <QtQuickTemplates2/private/qquickshortcutcontext_p_p.h>
+#include <QtQuickTemplates2/private/qquickaction_p.h>
+#include <QtQuickTemplates2/private/qquickmenu_p_p.h>
+#include <QtQuickTemplates2/private/qquicknativeiconloader_p.h>
+#include <QtQuickTemplates2/private/qquickshortcutcontext_p_p.h>
+
+QT_BEGIN_NAMESPACE
+
+Q_LOGGING_CATEGORY(lcNativeMenuItem, "qt.quick.controls.nativemenuitem")
+
+/*!
+ \class QQuickNativeMenuItem
+ \brief A native menu item.
+ \since 6.7
+ \internal
+
+ Provides a way to sync between the properties and signals of action
+ and the underlying native menu item.
+
+ \sa Menu, Action
+*/
+
+/*!
+ \internal
+
+ Adds \a action as a menu item of \a parentMenu.
+*/
+QQuickNativeMenuItem::QQuickNativeMenuItem(QQuickMenu *parentMenu, QQuickAction *action)
+ : QObject(parentMenu)
+ , m_parentMenu(parentMenu)
+ , m_action(action)
+{
+}
+
+/*!
+ \internal
+
+ Adds \subMenu as a sub-menu menu item of \a parentMenu.
+*/
+QQuickNativeMenuItem::QQuickNativeMenuItem(QQuickMenu *parentMenu, QQuickMenu *subMenu)
+ : QObject(parentMenu)
+ , m_parentMenu(parentMenu)
+ , m_subMenu(subMenu)
+{
+}
+
+QQuickNativeMenuItem::~QQuickNativeMenuItem()
+{
+ qCDebug(lcNativeMenuItem) << "destroying" << debugText();
+}
+
+QQuickAction *QQuickNativeMenuItem::action() const
+{
+ return m_action;
+}
+
+QQuickMenu *QQuickNativeMenuItem::subMenu() const
+{
+ return m_subMenu;
+}
+
+void QQuickNativeMenuItem::clearSubMenu()
+{
+ m_subMenu = nullptr;
+ m_handle->setMenu(nullptr);
+}
+
+QPlatformMenuItem *QQuickNativeMenuItem::handle() const
+{
+ return m_handle.get();
+}
+
+QPlatformMenuItem *QQuickNativeMenuItem::create()
+{
+ qCDebug(lcNativeMenuItem) << "create called on" << debugText() << "m_handle" << m_handle.get();
+ if (m_handle)
+ return m_handle.get();
+
+ auto *parentMenuPrivate = QQuickMenuPrivate::get(m_parentMenu);
+ m_handle.reset(parentMenuPrivate->nativeHandle->createMenuItem());
+
+ if (!m_handle)
+ m_handle.reset(QGuiApplicationPrivate::platformTheme()->createPlatformMenuItem());
+
+ Q_ASSERT(m_action || m_subMenu);
+
+ if (m_handle) {
+ if (m_action) {
+ connect(m_handle.get(), &QPlatformMenuItem::activated, m_action, [this](){
+ m_action->trigger(m_parentMenu);
+ });
+ } else { // m_subMenu
+// m_handle->setMenu(parentMenuPrivate->nativeHandle.get());
+ m_handle->setMenu(QQuickMenuPrivate::get(m_subMenu)->nativeHandle.get());
+ // TODO: do we need to call anything here? need to at least ensure
+ // that the QQuickMenu::isVisible returns true after this
+// connect(m_handle.get(), &QPlatformMenuItem::activated, m_subMenu, &QQuickMenu::?
+ }
+ }
+
+ return m_handle.get();
+}
+
+void QQuickNativeMenuItem::sync()
+{
+ qCDebug(lcNativeMenuItem) << "sync called on" << debugText() << "handle" << m_handle.get();
+ if (/* !m_complete || */!create())
+ return;
+
+ Q_ASSERT(m_action || m_subMenu);
+
+ m_handle->setEnabled(m_action ? m_action->isEnabled() : m_subMenu->isEnabled());
+// m_handle->setVisible(isVisible());
+// m_handle->setIsSeparator(m_separator);
+ m_handle->setCheckable(m_action && m_action->isCheckable());
+ m_handle->setChecked(m_action && m_action->isChecked());
+ m_handle->setRole(QPlatformMenuItem::TextHeuristicRole);
+ m_handle->setText(m_action ? m_action->text() : m_subMenu->title());
+
+// m_handle->setFont(m_font);
+// m_handle->setHasExclusiveGroup(m_group && m_group->isExclusive());
+ m_handle->setHasExclusiveGroup(false);
+
+ if (m_iconLoader)
+ m_handle->setIcon(m_iconLoader->toQIcon());
+
+ if (m_subMenu) {
+ // Sync first as dynamically created menus may need to get the handle recreated.
+ auto *subMenuPrivate = QQuickMenuPrivate::get(m_subMenu);
+ subMenuPrivate->syncWithNativeMenu();
+ if (subMenuPrivate->nativeHandle)
+ m_handle->setMenu(subMenuPrivate->nativeHandle.get());
+ }
+
+#if QT_CONFIG(shortcut)
+ if (m_action)
+ m_handle->setShortcut(m_action->shortcut());
+#endif
+
+ if (m_parentMenu) {
+ auto *menuPrivate = QQuickMenuPrivate::get(m_parentMenu);
+ if (menuPrivate->nativeHandle)
+ menuPrivate->nativeHandle->syncMenuItem(m_handle.get());
+ }
+}
+
+QQuickNativeIconLoader *QQuickNativeMenuItem::iconLoader() const
+{
+ if (!m_iconLoader) {
+ QQuickNativeMenuItem *that = const_cast<QQuickNativeMenuItem *>(this);
+ static int slot = staticMetaObject.indexOfSlot("updateIcon()");
+ m_iconLoader = new QQuickNativeIconLoader(slot, that);
+// m_iconLoader->setEnabled(m_complete);
+ }
+ return m_iconLoader;
+}
+
+void QQuickNativeMenuItem::updateIcon()
+{
+ sync();
+}
+
+void QQuickNativeMenuItem::addShortcut()
+{
+#if QT_CONFIG(shortcut)
+ const QKeySequence sequence = m_action->shortcut();
+ if (!sequence.isEmpty() && m_action->isEnabled()) {
+ m_shortcutId = QGuiApplicationPrivate::instance()->shortcutMap.addShortcut(this, sequence,
+ Qt::WindowShortcut, QQuickShortcutContext::matcher);
+ } else {
+ m_shortcutId = -1;
+ }
+#endif
+}
+
+void QQuickNativeMenuItem::removeShortcut()
+{
+#if QT_CONFIG(shortcut)
+ if (m_shortcutId == -1)
+ return;
+
+ const QKeySequence sequence = m_action->shortcut();
+ QGuiApplicationPrivate::instance()->shortcutMap.removeShortcut(m_shortcutId, this, sequence);
+#endif
+}
+
+QString QQuickNativeMenuItem::debugText() const
+{
+ return m_action ? m_action->text() : (m_subMenu ? m_subMenu->title() : QStringLiteral("(No text)"));
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qquicknativemenuitem_p.cpp"
diff --git a/src/quicktemplates/qquicknativemenuitem_p.h b/src/quicktemplates/qquicknativemenuitem_p.h
new file mode 100644
index 0000000000..d317e64e44
--- /dev/null
+++ b/src/quicktemplates/qquicknativemenuitem_p.h
@@ -0,0 +1,64 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQUICKNATIVEMENUITEM_P_H
+#define QQUICKNATIVEMENUITEM_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtCore/qobject.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQuickAction;
+class QQuickNativeIconLoader;
+class QQuickMenu;
+class QPlatformMenuItem;
+
+class QQuickNativeMenuItem : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit QQuickNativeMenuItem(QQuickMenu *parentMenu, QQuickAction *action);
+ explicit QQuickNativeMenuItem(QQuickMenu *parentMenu, QQuickMenu *subMenu);
+ ~QQuickNativeMenuItem();
+
+ QQuickAction *action() const;
+ QQuickMenu *subMenu() const;
+ void clearSubMenu();
+ QPlatformMenuItem *handle() const;
+ QPlatformMenuItem *create();
+ void sync();
+
+ QQuickNativeIconLoader *iconLoader() const;
+
+ QString debugText() const;
+
+private Q_SLOTS:
+ void updateIcon();
+
+private:
+ void addShortcut();
+ void removeShortcut();
+
+ QQuickMenu *m_parentMenu = nullptr;
+ QQuickMenu *m_subMenu = nullptr;
+ QQuickAction *m_action = nullptr;
+ mutable QQuickNativeIconLoader *m_iconLoader = nullptr;
+ std::unique_ptr<QPlatformMenuItem> m_handle = nullptr;
+ int m_shortcutId = -1;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQUICKNATIVEMENUITEM_P_H
diff --git a/tests/auto/quickcontrols/CMakeLists.txt b/tests/auto/quickcontrols/CMakeLists.txt
index f4f2e6b2c6..6b53bdf80e 100644
--- a/tests/auto/quickcontrols/CMakeLists.txt
+++ b/tests/auto/quickcontrols/CMakeLists.txt
@@ -17,6 +17,12 @@ if(NOT ANDROID) # QTBUG-100258
add_subdirectory(focus)
endif()
add_subdirectory(font)
+# For now there's no way of knowing (at built time) if the Linux we're on is
+# running with the GTK+ platform theme, which is the only context
+# in which native menus are supported there. So we don't include it.
+if(WIN32 OR MACOS OR IOS OR ANDROID)
+ add_subdirectory(nativemenus)
+endif()
add_subdirectory(palette)
add_subdirectory(platform)
add_subdirectory(pointerhandlers)
diff --git a/tests/auto/quickcontrols/nativemenus/CMakeLists.txt b/tests/auto/quickcontrols/nativemenus/CMakeLists.txt
new file mode 100644
index 0000000000..2112c9ab74
--- /dev/null
+++ b/tests/auto/quickcontrols/nativemenus/CMakeLists.txt
@@ -0,0 +1,43 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+if (NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
+ cmake_minimum_required(VERSION 3.16)
+ project(tst_nativemenus LANGUAGES C CXX ASM)
+ find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
+endif()
+
+# Collect test data
+file(GLOB_RECURSE test_data_glob
+ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
+ data/*)
+list(APPEND test_data ${test_data_glob})
+
+qt_internal_add_test(tst_nativemenus
+ SOURCES
+ tst_nativemenus.cpp
+ LIBRARIES
+ Qt::CorePrivate
+ Qt::Gui
+ Qt::GuiPrivate
+ Qt::QmlPrivate
+ Qt::QuickControls2
+ Qt::QuickControls2Private
+ Qt::QuickControlsTestUtilsPrivate
+ Qt::QuickPrivate
+ Qt::QuickTemplates2Private
+ Qt::QuickTest
+ Qt::QuickTestUtilsPrivate
+ Qt::TestPrivate
+ TESTDATA ${test_data}
+)
+
+qt_internal_extend_target(tst_nativemenus CONDITION ANDROID OR IOS
+ DEFINES
+ QT_QMLTEST_DATADIR=":/data"
+)
+
+qt_internal_extend_target(tst_nativemenus CONDITION NOT ANDROID AND NOT IOS
+ DEFINES
+ QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data"
+)
diff --git a/tests/auto/quickcontrols/nativemenus/data/emptyMenu.qml b/tests/auto/quickcontrols/nativemenus/data/emptyMenu.qml
new file mode 100644
index 0000000000..f1d4f33625
--- /dev/null
+++ b/tests/auto/quickcontrols/nativemenus/data/emptyMenu.qml
@@ -0,0 +1,50 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Templates as T
+import QtQuick.Controls
+
+ApplicationWindow {
+ width: 400
+ height: 400
+
+ property alias contextMenu: contextMenu
+
+ function addAction(menu: T.Menu, text: string) {
+ menu.addAction(actionComponent.createObject(null, { text: text }))
+ }
+
+ function insertAction(menu: T.Menu, index: int, text: string) {
+ menu.insertAction(index, actionComponent.createObject(null, { text: text }))
+ }
+
+ function removeAction(menu: T.Menu, index: int) {
+ menu.removeAction(menu.actionAt(index))
+ }
+
+ function addMenu(menu: T.Menu, title: string) {
+ menu.addMenu(menuComponent.createObject(null, { title: title }))
+ }
+
+ Component {
+ id: actionComponent
+
+ Action {
+ objectName: text
+ }
+ }
+
+ Component {
+ id: menuComponent
+
+ Menu {
+ objectName: title
+ }
+ }
+
+ Menu {
+ id: contextMenu
+ objectName: "menu"
+ }
+}
diff --git a/tests/auto/quickcontrols/nativemenus/data/staticActionsAndSubmenus.qml b/tests/auto/quickcontrols/nativemenus/data/staticActionsAndSubmenus.qml
new file mode 100644
index 0000000000..54fb1497d0
--- /dev/null
+++ b/tests/auto/quickcontrols/nativemenus/data/staticActionsAndSubmenus.qml
@@ -0,0 +1,56 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Templates as T
+import QtQuick.Controls
+
+ApplicationWindow {
+ width: 400
+ height: 400
+
+ property alias contextMenu: contextMenu
+// property alias buttonMenu: buttonMenu
+
+ Menu {
+ id: contextMenu
+ objectName: "menu"
+
+ Action {
+ objectName: text
+ text: "action1"
+ shortcut: "A"
+ }
+
+ Action {
+ objectName: text
+ text: "action2"
+ shortcut: "B"
+ }
+
+ Menu {
+ id: subMenu
+ objectName: "subMenu"
+
+ Action {
+ objectName: text
+ text: "subAction1"
+ shortcut: "1"
+ }
+ }
+ }
+
+// Button {
+// text: "Menu button"
+
+// Menu {
+// id: buttonMenu
+
+// Action {
+// objectName: text
+// text: "buttonMenuAction1"
+// shortcut: "Z"
+// }
+// }
+// }
+}
diff --git a/tests/auto/quickcontrols/nativemenus/tst_nativemenus.cpp b/tests/auto/quickcontrols/nativemenus/tst_nativemenus.cpp
new file mode 100644
index 0000000000..4e1fe9dd70
--- /dev/null
+++ b/tests/auto/quickcontrols/nativemenus/tst_nativemenus.cpp
@@ -0,0 +1,181 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include <QtTest/qtest.h>
+#include <QtTest/qsignalspy.h>
+#include <QtGui/qcursor.h>
+#if QT_CONFIG(shortcut)
+#include <QtGui/qkeysequence.h>
+#endif
+#include <QtGui/qstylehints.h>
+#include <QtGui/qpa/qplatformintegration.h>
+#include <QtGui/private/qguiapplication_p.h>
+#include <QtQml/qqmlengine.h>
+#include <QtQml/qqmlcomponent.h>
+#include <QtQml/qqmlcontext.h>
+#include <QtQuick/qquickview.h>
+#include <QtQuick/private/qquickitem_p.h>
+#include <QtQuickTestUtils/private/qmlutils_p.h>
+#include <QtQuickTestUtils/private/visualtestutils_p.h>
+#include <QtQuickControlsTestUtils/private/controlstestutils_p.h>
+#include <QtQuickControlsTestUtils/private/qtest_quickcontrols_p.h>
+
+#include <QtQuickTemplates2/private/qquickaction_p.h>
+#include <QtQuickTemplates2/private/qquickapplicationwindow_p.h>
+#include <QtQuickTemplates2/private/qquickmenu_p.h>
+#include <QtQuickTemplates2/private/qquickmenu_p_p.h>
+#include <QtQuickTemplates2/private/qquickmenuitem_p.h>
+#include <QtQuickTemplates2/private/qquickmenuseparator_p.h>
+
+using namespace QQuickVisualTestUtils;
+using namespace QQuickControlsTestUtils;
+
+/*
+ We have a separate test project for native menus because we don't
+ want to run them for every style, just the platforms that have
+ native menu support.
+*/
+
+class tst_NativeMenus : public QQmlDataTest
+{
+ Q_OBJECT
+
+public:
+ tst_NativeMenus();
+
+private slots:
+ void defaults();
+ void staticActionsAndSubmenus();
+ void dynamicActions();
+ void dynamicSubmenus();
+};
+
+tst_NativeMenus::tst_NativeMenus()
+ : QQmlDataTest(QT_QMLTEST_DATADIR)
+{
+ qputenv("QT_QUICK_CONTROLS_USE_NATIVE_MENUS", "1");
+}
+
+void tst_NativeMenus::defaults()
+{
+ QQuickControlsApplicationHelper helper(this, QLatin1String("emptyMenu.qml"));
+ QVERIFY2(helper.ready, helper.failureMessage());
+ QQuickApplicationWindow *window = helper.appWindow;
+ window->show();
+ QVERIFY(QTest::qWaitForWindowExposed(window));
+
+ QQuickMenu *contextMenu = window->property("contextMenu").value<QQuickMenu*>();
+ QVERIFY(contextMenu);
+ auto *contextMenuPrivate = QQuickMenuPrivate::get(contextMenu);
+ QVERIFY(contextMenuPrivate->usingNativeMenu());
+}
+
+void tst_NativeMenus::staticActionsAndSubmenus()
+{
+ QQuickControlsApplicationHelper helper(this, QLatin1String("staticActionsAndSubmenus.qml"));
+ QVERIFY2(helper.ready, helper.failureMessage());
+ QQuickApplicationWindow *window = helper.appWindow;
+ window->show();
+ QVERIFY(QTest::qWaitForWindowExposed(window));
+
+ QQuickMenu *contextMenu = window->property("contextMenu").value<QQuickMenu*>();
+ QVERIFY(contextMenu);
+ QVERIFY(contextMenu->requestNative());
+ auto *contextMenuPrivate = QQuickMenuPrivate::get(contextMenu);
+
+ // Check that the actions of the parent menu can be accessed
+ // and are in the appropriate places in contentData.
+ auto *action1 = contextMenu->actionAt(0);
+ QVERIFY(action1);
+ QCOMPARE(contextMenuPrivate->contentData.at(0), action1);
+
+ auto *action2 = contextMenu->actionAt(1);
+ QVERIFY(action2);
+ QCOMPARE(contextMenuPrivate->contentData.at(1), action2);
+
+ // Check that the sub-menu can be accessed and is in the
+ // appropriate place in contentData.
+ auto *subMenu = contextMenu->menuAt(2);
+ QVERIFY(subMenu);
+
+ // TODO: check that sub-menus exist
+}
+
+void tst_NativeMenus::dynamicActions()
+{
+ QQuickControlsApplicationHelper helper(this, QLatin1String("emptyMenu.qml"));
+ QVERIFY2(helper.ready, helper.failureMessage());
+ QQuickApplicationWindow *window = helper.appWindow;
+ window->show();
+ QVERIFY(QTest::qWaitForWindowExposed(window));
+
+ QQuickMenu *contextMenu = window->property("contextMenu").value<QQuickMenu*>();
+ QVERIFY(contextMenu);
+ auto *contextMenuPrivate = QQuickMenuPrivate::get(contextMenu);
+
+ // Check that items can be appended to an empty menu.
+ QCOMPARE(contextMenu->actionAt(0), nullptr);
+ QVERIFY(QMetaObject::invokeMethod(window, "addAction",
+ Q_ARG(QQuickMenu *, contextMenu), Q_ARG(QString, "action1")));
+ {
+ auto action1 = contextMenu->actionAt(0);
+ QVERIFY(action1);
+ QCOMPARE(action1->text(), "action1");
+ QCOMPARE(contextMenuPrivate->contentData.at(0), action1);
+ }
+
+ // Check that actions can be appended after existing items in the parent menu.
+ QCOMPARE(contextMenu->actionAt(1), nullptr);
+ QVERIFY(QMetaObject::invokeMethod(window, "addAction",
+ Q_ARG(QQuickMenu *, contextMenu), Q_ARG(QString, "action2")));
+ {
+ auto action2 = contextMenu->actionAt(1);
+ QVERIFY(action2);
+ QCOMPARE(action2->text(), "action2");
+ QCOMPARE(contextMenuPrivate->contentData.at(1), action2);
+ }
+
+ // Check that actions can be inserted before existing items in the parent menu.
+ QVERIFY(QMetaObject::invokeMethod(window, "insertAction",
+ Q_ARG(QQuickMenu *, contextMenu), Q_ARG(int, 0), Q_ARG(QString, "action0")));
+ {
+ auto action0 = contextMenu->actionAt(0);
+ QVERIFY(action0);
+ QCOMPARE(action0->text(), "action0");
+ QCOMPARE(contextMenuPrivate->contentData.at(0), action0);
+ }
+}
+
+void tst_NativeMenus::dynamicSubmenus()
+{
+ QQuickControlsApplicationHelper helper(this, QLatin1String("emptyMenu.qml"));
+ QVERIFY2(helper.ready, helper.failureMessage());
+ QQuickApplicationWindow *window = helper.appWindow;
+ window->show();
+ QVERIFY(QTest::qWaitForWindowExposed(window));
+
+ QQuickMenu *contextMenu = window->property("contextMenu").value<QQuickMenu*>();
+ QVERIFY(contextMenu);
+ auto *contextMenuPrivate = QQuickMenuPrivate::get(contextMenu);
+
+ // Check that sub-menus (with no menu items, yet) can be appended to an empty parent menu.
+ QVERIFY(QMetaObject::invokeMethod(window, "addMenu",
+ Q_ARG(QQuickMenu *, contextMenu), Q_ARG(QString, "subMenu1")));
+ auto subMenu1 = contextMenu->menuAt(0);
+ QVERIFY(subMenu1);
+ QCOMPARE(subMenu1->title(), "subMenu1");
+ QCOMPARE(contextMenuPrivate->contentData.at(0), subMenu1);
+
+ //
+ QVERIFY(QMetaObject::invokeMethod(window, "addAction",
+ Q_ARG(QQuickMenu *, subMenu1), Q_ARG(QString, "subMenuAction1")));
+
+ // TODO: insert another sub-menu action before the first one
+}
+
+// TODO: add a test that mixes items with native items
+// and ensure that all items are recreated as non-native
+
+QTEST_MAIN(tst_NativeMenus)
+
+#include "tst_nativemenus.moc"
diff --git a/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp b/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp
index fbb4e7d5f9..d446fbecac 100644
--- a/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp
+++ b/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp
@@ -32,6 +32,8 @@
using namespace QQuickVisualTestUtils;
using namespace QQuickControlsTestUtils;
+// Native menu tests are in "nativemenus".
+
class tst_QQuickMenu : public QQmlDataTest
{
Q_OBJECT