// Copyright (C) 2020 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 "qquicklabsplatformmenu_p.h" #include "qquicklabsplatformmenubar_p.h" #include "qquicklabsplatformmenuitem_p.h" #include "qquicklabsplatformiconloader_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "widgets/qwidgetplatform_p.h" #if QT_CONFIG(systemtrayicon) #include "qquicklabsplatformsystemtrayicon_p.h" #endif QT_BEGIN_NAMESPACE /*! \qmltype Menu \inherits QtObject //! \nativetype QQuickLabsPlatformMenu \inqmlmodule Qt.labs.platform \since 5.8 \brief A native menu. The Menu type provides a QML API for native platform menu popups. \image qtlabsplatform-menu.png Menu can be used in a \l MenuBar, or as a stand-alone context menu. The following example shows how to open a context menu on right mouse click: \code MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton onClicked: zoomMenu.open() } Menu { id: zoomMenu MenuItem { text: qsTr("Zoom In") shortcut: StandardKey.ZoomIn onTriggered: zoomIn() } MenuItem { text: qsTr("Zoom Out") shortcut: StandardKey.ZoomOut onTriggered: zoomOut() } } \endcode \section2 Submenus To create submenus, declare a Menu as a child of another Menu: \qml Menu { title: qsTr("Edit") Menu { title: qsTr("Advanced") MenuItem { text: qsTr("Auto-indent Selection") onTriggered: autoIndentSelection() } MenuItem { text: qsTr("Rewrap Paragraph") onTriggered: rewrapParagraph() } } } \endqml \section2 Dynamically generating menu items You can dynamically generate menu items with \l Instantiator. The following code shows how you can implement a "Recent Files" submenu, where the items come from a list of files stored in settings: \snippet qtlabsplatform-menu-instantiator.qml menu \section2 Availability A native platform menu is currently available on the following platforms: \list \li macOS \li iOS \li Android \li Linux (only available as a stand-alone context menu when running with the GTK+ platform theme) \endlist \input includes/widgets.qdocinc 1 \labs \sa MenuItem, MenuSeparator, MenuBar */ /*! \qmlsignal Qt.labs.platform::Menu::aboutToShow() This signal is emitted when the menu is about to be shown to the user. */ /*! \qmlsignal Qt.labs.platform::Menu::aboutToHide() This signal is emitted when the menu is about to be hidden from the user. */ Q_LOGGING_CATEGORY(qtLabsPlatformMenus, "qt.labs.platform.menus") QQuickLabsPlatformMenu::QQuickLabsPlatformMenu(QObject *parent) : QObject(parent), m_complete(false), m_enabled(true), m_visible(true), m_minimumWidth(-1), m_type(QPlatformMenu::DefaultMenu), m_menuBar(nullptr), m_parentMenu(nullptr), m_systemTrayIcon(nullptr), m_menuItem(nullptr), m_iconLoader(nullptr), m_handle(nullptr) { } QQuickLabsPlatformMenu::~QQuickLabsPlatformMenu() { if (m_menuBar) m_menuBar->removeMenu(this); if (m_parentMenu) m_parentMenu->removeMenu(this); unparentSubmenus(); delete m_iconLoader; m_iconLoader = nullptr; delete m_handle; m_handle = nullptr; } void QQuickLabsPlatformMenu::unparentSubmenus() { for (QQuickLabsPlatformMenuItem *item : std::as_const(m_items)) { if (QQuickLabsPlatformMenu *subMenu = item->subMenu()) subMenu->setParentMenu(nullptr); item->setMenu(nullptr); } } QPlatformMenu *QQuickLabsPlatformMenu::handle() const { return m_handle; } QPlatformMenu * QQuickLabsPlatformMenu::create() { if (!m_handle) { if (m_menuBar && m_menuBar->handle()) m_handle = m_menuBar->handle()->createMenu(); else if (m_parentMenu && m_parentMenu->handle()) m_handle = m_parentMenu->handle()->createSubMenu(); #if QT_CONFIG(systemtrayicon) else if (m_systemTrayIcon && m_systemTrayIcon->handle()) m_handle = m_systemTrayIcon->handle()->createMenu(); #endif // TODO: implement ^ // - QCocoaMenuBar::createMenu() // - QCocoaMenu::createSubMenu() // - QCocoaSystemTrayIcon::createMenu() if (!m_handle) m_handle = QGuiApplicationPrivate::platformTheme()->createPlatformMenu(); if (!m_handle) m_handle = QWidgetPlatform::createMenu(); qCDebug(qtLabsPlatformMenus) << "Menu ->" << m_handle; if (m_handle) { connect(m_handle, &QPlatformMenu::aboutToShow, this, &QQuickLabsPlatformMenu::aboutToShow); connect(m_handle, &QPlatformMenu::aboutToHide, this, &QQuickLabsPlatformMenu::aboutToHide); for (QQuickLabsPlatformMenuItem *item : std::as_const(m_items)) m_handle->insertMenuItem(item->create(), nullptr); if (m_menuItem) { if (QPlatformMenuItem *handle = m_menuItem->create()) handle->setMenu(m_handle); } } } return m_handle; } void QQuickLabsPlatformMenu::destroy() { if (!m_handle) return; // Ensure that all submenus are unparented before we are destroyed, // so that they don't try to access a destroyed menu. unparentSubmenus(); delete m_handle; m_handle = nullptr; } void QQuickLabsPlatformMenu::sync() { if (!m_complete || !create()) return; m_handle->setText(m_title); m_handle->setEnabled(m_enabled); m_handle->setVisible(m_visible); m_handle->setMinimumWidth(m_minimumWidth); m_handle->setMenuType(m_type); m_handle->setFont(m_font); if (m_menuBar && m_menuBar->handle()) m_menuBar->handle()->syncMenu(m_handle); #if QT_CONFIG(systemtrayicon) else if (m_systemTrayIcon && m_systemTrayIcon->handle()) m_systemTrayIcon->handle()->updateMenu(m_handle); #endif for (QQuickLabsPlatformMenuItem *item : std::as_const(m_items)) item->sync(); } /*! \qmldefault \qmlproperty list Qt.labs.platform::Menu::data This default property holds the list of all objects declared as children of the menu. The data property includes objects that are not \l MenuItem instances, such as \l Timer and \l QtObject. \sa items */ QQmlListProperty QQuickLabsPlatformMenu::data() { return QQmlListProperty(this, nullptr, data_append, data_count, data_at, data_clear); } /*! \qmlproperty list Qt.labs.platform::Menu::items This property holds the list of items in the menu. */ QQmlListProperty QQuickLabsPlatformMenu::items() { return QQmlListProperty(this, nullptr, items_append, items_count, items_at, items_clear); } /*! \readonly \qmlproperty MenuBar Qt.labs.platform::Menu::menuBar This property holds the menubar that the menu belongs to, or \c null if the menu is not in a menubar. */ QQuickLabsPlatformMenuBar *QQuickLabsPlatformMenu::menuBar() const { return m_menuBar; } void QQuickLabsPlatformMenu::setMenuBar(QQuickLabsPlatformMenuBar *menuBar) { if (m_menuBar == menuBar) return; m_menuBar = menuBar; destroy(); emit menuBarChanged(); } /*! \readonly \qmlproperty Menu Qt.labs.platform::Menu::parentMenu This property holds the parent menu that the menu belongs to, or \c null if the menu is not a sub-menu. */ QQuickLabsPlatformMenu *QQuickLabsPlatformMenu::parentMenu() const { return m_parentMenu; } void QQuickLabsPlatformMenu::setParentMenu(QQuickLabsPlatformMenu *menu) { if (m_parentMenu == menu) return; m_parentMenu = menu; destroy(); emit parentMenuChanged(); } #if QT_CONFIG(systemtrayicon) /*! \readonly \qmlproperty SystemTrayIcon Qt.labs.platform::Menu::systemTrayIcon This property holds the system tray icon that the menu belongs to, or \c null if the menu is not in a system tray icon. */ QQuickLabsPlatformSystemTrayIcon *QQuickLabsPlatformMenu::systemTrayIcon() const { return m_systemTrayIcon; } void QQuickLabsPlatformMenu::setSystemTrayIcon(QQuickLabsPlatformSystemTrayIcon *icon) { if (m_systemTrayIcon == icon) return; m_systemTrayIcon = icon; destroy(); emit systemTrayIconChanged(); } #endif /*! \readonly \qmlproperty MenuItem Qt.labs.platform::Menu::menuItem This property holds the item that presents the menu (in a parent menu). */ QQuickLabsPlatformMenuItem *QQuickLabsPlatformMenu::menuItem() const { if (!m_menuItem) { QQuickLabsPlatformMenu *that = const_cast(this); m_menuItem = new QQuickLabsPlatformMenuItem(that); m_menuItem->setSubMenu(that); m_menuItem->setText(m_title); m_menuItem->setIcon(icon()); m_menuItem->setVisible(m_visible); m_menuItem->setEnabled(m_enabled); m_menuItem->componentComplete(); } return m_menuItem; } /*! \qmlproperty bool Qt.labs.platform::Menu::enabled This property holds whether the menu is enabled. The default value is \c true. */ bool QQuickLabsPlatformMenu::isEnabled() const { return m_enabled; } void QQuickLabsPlatformMenu::setEnabled(bool enabled) { if (m_enabled == enabled) return; if (m_menuItem) m_menuItem->setEnabled(enabled); m_enabled = enabled; sync(); emit enabledChanged(); } /*! \qmlproperty bool Qt.labs.platform::Menu::visible This property holds whether the menu is visible. The default value is \c true. */ bool QQuickLabsPlatformMenu::isVisible() const { return m_visible; } void QQuickLabsPlatformMenu::setVisible(bool visible) { if (m_visible == visible) return; if (m_menuItem) m_menuItem->setVisible(visible); m_visible = visible; sync(); emit visibleChanged(); } /*! \qmlproperty int Qt.labs.platform::Menu::minimumWidth This property holds the minimum width of the menu. The default value is \c -1 (no minimum width). */ int QQuickLabsPlatformMenu::minimumWidth() const { return m_minimumWidth; } void QQuickLabsPlatformMenu::setMinimumWidth(int width) { if (m_minimumWidth == width) return; m_minimumWidth = width; sync(); emit minimumWidthChanged(); } /*! \qmlproperty enumeration Qt.labs.platform::Menu::type This property holds the type of the menu. Available values: \value Menu.DefaultMenu A normal menu (default). \value Menu.EditMenu An edit menu with pre-populated cut, copy and paste items. */ QPlatformMenu::MenuType QQuickLabsPlatformMenu::type() const { return m_type; } void QQuickLabsPlatformMenu::setType(QPlatformMenu::MenuType type) { if (m_type == type) return; m_type = type; sync(); emit typeChanged(); } /*! \qmlproperty string Qt.labs.platform::Menu::title This property holds the menu's title. */ QString QQuickLabsPlatformMenu::title() const { return m_title; } void QQuickLabsPlatformMenu::setTitle(const QString &title) { if (m_title == title) return; if (m_menuItem) m_menuItem->setText(title); m_title = title; sync(); emit titleChanged(); } /*! \qmlproperty font Qt.labs.platform::Menu::font This property holds the menu's font. \sa title */ QFont QQuickLabsPlatformMenu::font() const { return m_font; } void QQuickLabsPlatformMenu::setFont(const QFont& font) { if (m_font == font) return; m_font = font; sync(); emit fontChanged(); } /*! \since Qt.labs.platform 1.1 (Qt 5.12) \qmlproperty url Qt.labs.platform::Menu::icon.source \qmlproperty string Qt.labs.platform::Menu::icon.name \qmlproperty bool Qt.labs.platform::Menu::icon.mask This property holds the menu item's icon. */ QQuickLabsPlatformIcon QQuickLabsPlatformMenu::icon() const { if (!m_iconLoader) return QQuickLabsPlatformIcon(); return iconLoader()->icon(); } void QQuickLabsPlatformMenu::setIcon(const QQuickLabsPlatformIcon &icon) { if (iconLoader()->icon() == icon) return; if (m_menuItem) m_menuItem->setIcon(icon); iconLoader()->setIcon(icon); emit iconChanged(); } /*! \qmlmethod void Qt.labs.platform::Menu::addItem(MenuItem item) Adds an \a item to the end of the menu. */ void QQuickLabsPlatformMenu::addItem(QQuickLabsPlatformMenuItem *item) { insertItem(m_items.size(), item); } /*! \qmlmethod void Qt.labs.platform::Menu::insertItem(int index, MenuItem item) Inserts an \a item at the specified \a index in the menu. */ void QQuickLabsPlatformMenu::insertItem(int index, QQuickLabsPlatformMenuItem *item) { if (!item || m_items.contains(item)) return; m_items.insert(index, item); m_data.append(item); item->setMenu(this); if (m_handle && item->create()) { QQuickLabsPlatformMenuItem *before = m_items.value(index + 1); m_handle->insertMenuItem(item->handle(), before ? before->create() : nullptr); } sync(); emit itemsChanged(); } /*! \qmlmethod void Qt.labs.platform::Menu::removeItem(MenuItem item) Removes an \a item from the menu. */ void QQuickLabsPlatformMenu::removeItem(QQuickLabsPlatformMenuItem *item) { if (!item || !m_items.removeOne(item)) return; m_data.removeOne(item); if (m_handle) m_handle->removeMenuItem(item->handle()); item->setMenu(nullptr); sync(); emit itemsChanged(); } /*! \qmlmethod void Qt.labs.platform::Menu::addMenu(Menu submenu) Adds a \a submenu to the end of the menu. */ void QQuickLabsPlatformMenu::addMenu(QQuickLabsPlatformMenu *menu) { insertMenu(m_items.size(), menu); } /*! \qmlmethod void Qt.labs.platform::Menu::insertMenu(int index, Menu submenu) Inserts a \a submenu at the specified \a index in the menu. */ void QQuickLabsPlatformMenu::insertMenu(int index, QQuickLabsPlatformMenu *menu) { if (!menu) return; menu->setParentMenu(this); insertItem(index, menu->menuItem()); } /*! \qmlmethod void Qt.labs.platform::Menu::removeMenu(Menu submenu) Removes a \a submenu from the menu. */ void QQuickLabsPlatformMenu::removeMenu(QQuickLabsPlatformMenu *menu) { if (!menu) return; menu->setParentMenu(nullptr); removeItem(menu->menuItem()); } /*! \qmlmethod void Qt.labs.platform::Menu::clear() Removes all items from the menu. */ void QQuickLabsPlatformMenu::clear() { if (m_items.isEmpty()) return; for (QQuickLabsPlatformMenuItem *item : std::as_const(m_items)) { m_data.removeOne(item); if (m_handle) m_handle->removeMenuItem(item->handle()); item->setMenu(nullptr); delete item; } m_items.clear(); sync(); emit itemsChanged(); } /*! \qmlmethod void Qt.labs.platform::Menu::open(MenuItem item) Opens the menu at the current mouse position, optionally aligned to a menu \a item. */ /*! \qmlmethod void Qt.labs.platform::Menu::open(Item target, MenuItem item) Opens the menu at the specified \a target item, optionally aligned to a menu \a item. */ void QQuickLabsPlatformMenu::open(QQmlV4FunctionPtr args) { if (!m_handle) return; if (args->length() > 2) { args->v4engine()->throwTypeError(); return; } QV4::ExecutionEngine *v4 = args->v4engine(); QV4::Scope scope(v4); QQuickItem *targetItem = nullptr; if (args->length() > 0) { QV4::ScopedValue value(scope, (*args)[0]); QV4::Scoped object(scope, value->as()); if (object) targetItem = qobject_cast(object->object()); } QQuickLabsPlatformMenuItem *menuItem = nullptr; if (args->length() > 1) { QV4::ScopedValue value(scope, (*args)[1]); QV4::Scoped object(scope, value->as()); if (object) menuItem = qobject_cast(object->object()); } QPoint offset; QWindow *window = findWindow(targetItem, &offset); QRect targetRect; if (targetItem) { QRectF sceneBounds = targetItem->mapRectToScene(targetItem->boundingRect()); targetRect = sceneBounds.toAlignedRect().translated(offset); } else { #if QT_CONFIG(cursor) QPoint pos = QCursor::pos(); if (window) pos = window->mapFromGlobal(pos); targetRect.moveTo(pos); #endif } m_handle->showPopup(window, QHighDpi::toNativeLocalPosition(targetRect, window), menuItem ? menuItem->handle() : nullptr); } /*! \qmlmethod void Qt.labs.platform::Menu::close() Closes the menu. */ void QQuickLabsPlatformMenu::close() { if (m_handle) m_handle->dismiss(); } void QQuickLabsPlatformMenu::classBegin() { } void QQuickLabsPlatformMenu::componentComplete() { m_complete = true; if (m_handle && m_iconLoader) m_iconLoader->setEnabled(true); sync(); } QQuickLabsPlatformIconLoader *QQuickLabsPlatformMenu::iconLoader() const { if (!m_iconLoader) { QQuickLabsPlatformMenu *that = const_cast(this); static int slot = staticMetaObject.indexOfSlot("updateIcon()"); m_iconLoader = new QQuickLabsPlatformIconLoader(slot, that); m_iconLoader->setEnabled(m_complete); } return m_iconLoader; } static QWindow *effectiveWindow(QWindow *window, QPoint *offset) { QQuickWindow *quickWindow = qobject_cast(window); if (quickWindow) { QWindow *renderWindow = QQuickRenderControl::renderWindowFor(quickWindow, offset); if (renderWindow) return renderWindow; } return window; } QWindow *QQuickLabsPlatformMenu::findWindow(QQuickItem *target, QPoint *offset) const { if (target) return effectiveWindow(target->window(), offset); if (m_menuBar && m_menuBar->window()) return effectiveWindow(m_menuBar->window(), offset); QObject *obj = parent(); while (obj) { QWindow *window = qobject_cast(obj); if (window) return effectiveWindow(window, offset); QQuickItem *item = qobject_cast(obj); if (item && item->window()) return effectiveWindow(item->window(), offset); obj = obj->parent(); } return nullptr; } void QQuickLabsPlatformMenu::data_append(QQmlListProperty *property, QObject *object) { QQuickLabsPlatformMenu *menu = static_cast(property->object); if (QQuickLabsPlatformMenuItem *item = qobject_cast(object)) menu->addItem(item); else if (QQuickLabsPlatformMenu *subMenu = qobject_cast(object)) menu->addMenu(subMenu); else menu->m_data.append(object); } qsizetype QQuickLabsPlatformMenu::data_count(QQmlListProperty *property) { QQuickLabsPlatformMenu *menu = static_cast(property->object); return menu->m_data.size(); } QObject *QQuickLabsPlatformMenu::data_at(QQmlListProperty *property, qsizetype index) { QQuickLabsPlatformMenu *menu = static_cast(property->object); return menu->m_data.value(index); } void QQuickLabsPlatformMenu::data_clear(QQmlListProperty *property) { QQuickLabsPlatformMenu *menu = static_cast(property->object); menu->m_data.clear(); } void QQuickLabsPlatformMenu::items_append(QQmlListProperty *property, QQuickLabsPlatformMenuItem *item) { QQuickLabsPlatformMenu *menu = static_cast(property->object); menu->addItem(item); } qsizetype QQuickLabsPlatformMenu::items_count(QQmlListProperty *property) { QQuickLabsPlatformMenu *menu = static_cast(property->object); return menu->m_items.size(); } QQuickLabsPlatformMenuItem *QQuickLabsPlatformMenu::items_at(QQmlListProperty *property, qsizetype index) { QQuickLabsPlatformMenu *menu = static_cast(property->object); return menu->m_items.value(index); } void QQuickLabsPlatformMenu::items_clear(QQmlListProperty *property) { QQuickLabsPlatformMenu *menu = static_cast(property->object); menu->clear(); } void QQuickLabsPlatformMenu::updateIcon() { if (!m_handle || !m_iconLoader) return; m_handle->setIcon(m_iconLoader->toQIcon()); sync(); } QT_END_NAMESPACE #include "moc_qquicklabsplatformmenu_p.cpp"