aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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