diff options
-rw-r--r-- | src/quicktemplates/CMakeLists.txt | 6 | ||||
-rw-r--r-- | src/quicktemplates/qquickmenu.cpp | 454 | ||||
-rw-r--r-- | src/quicktemplates/qquickmenu_p.h | 11 | ||||
-rw-r--r-- | src/quicktemplates/qquickmenu_p_p.h | 22 | ||||
-rw-r--r-- | src/quicktemplates/qquicknativeicon.cpp | 50 | ||||
-rw-r--r-- | src/quicktemplates/qquicknativeicon_p.h | 57 | ||||
-rw-r--r-- | src/quicktemplates/qquicknativeiconloader.cpp | 68 | ||||
-rw-r--r-- | src/quicktemplates/qquicknativeiconloader_p.h | 54 | ||||
-rw-r--r-- | src/quicktemplates/qquicknativemenuitem.cpp | 207 | ||||
-rw-r--r-- | src/quicktemplates/qquicknativemenuitem_p.h | 64 | ||||
-rw-r--r-- | tests/auto/quickcontrols/CMakeLists.txt | 6 | ||||
-rw-r--r-- | tests/auto/quickcontrols/nativemenus/CMakeLists.txt | 43 | ||||
-rw-r--r-- | tests/auto/quickcontrols/nativemenus/data/emptyMenu.qml | 50 | ||||
-rw-r--r-- | tests/auto/quickcontrols/nativemenus/data/staticActionsAndSubmenus.qml | 56 | ||||
-rw-r--r-- | tests/auto/quickcontrols/nativemenus/tst_nativemenus.cpp | 181 | ||||
-rw-r--r-- | tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp | 2 |
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 |