// 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 "qquickmenuitem_p.h" #include //#include //#if QT_CONFIG(shortcut) //#include //#endif #include #include #include //#include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(lcNativeMenuItem, "qt.quick.controls.nativemenuitem") /*! \class QQuickNativeMenuItem \brief A native menu item. \since 6.7 \internal Creates a native menu item from an Action/MenuItem/Menu, and syncs the properties and signals. It can also represent a MenuSeparator. \sa Menu, Action */ QQuickNativeMenuItem *QQuickNativeMenuItem::createFromNonNativeItem( QQuickMenu *parentMenu, QQuickItem *nonNativeItem) { auto *menuItem = qobject_cast(nonNativeItem); Type type = Type::Unknown; if (menuItem) { if (menuItem->action()) { type = Type::Action; } else if (menuItem->subMenu()) { type = Type::SubMenu; } else { // It's a plain MenuItem, rather than a MenuItem created by us for an Action or Menu. type = Type::MenuItem; } } else if (qobject_cast(nonNativeItem)) { type = Type::Separator; } std::unique_ptr nativeMenuItemPtr(new QQuickNativeMenuItem( parentMenu, nonNativeItem, type)); if (type == Type::Unknown) { // It's not a Menu/Action/MenuSeparator that we're dealing with, but we still need // to create the QQuickNativeMenu item for it so that our container has the same // indices as the menu's contentModel. return nativeMenuItemPtr.release(); } qCDebug(lcNativeMenuItem) << "attemping to create native menu item for" << nativeMenuItemPtr->debugText(); auto *parentMenuPrivate = QQuickMenuPrivate::get(parentMenu); nativeMenuItemPtr->m_handle.reset(parentMenuPrivate->handle->createMenuItem()); if (!nativeMenuItemPtr->m_handle) nativeMenuItemPtr->m_handle.reset(QGuiApplicationPrivate::platformTheme()->createPlatformMenuItem()); if (!nativeMenuItemPtr->m_handle) return nullptr; auto *nativeMenuItem = nativeMenuItemPtr.release(); switch (type) { case Type::Action: // Ensure that the action is triggered when the user clicks on a native menu item. connect(nativeMenuItem->m_handle.get(), &QPlatformMenuItem::activated, nativeMenuItem->action(), [nativeMenuItem, parentMenu](){ nativeMenuItem->action()->trigger(parentMenu); }); // Handle programmatic changes in the Action. connect(nativeMenuItem->action(), &QQuickAction::textChanged, nativeMenuItem, &QQuickNativeMenuItem::sync); connect(nativeMenuItem->action(), &QQuickAction::iconChanged, nativeMenuItem, &QQuickNativeMenuItem::sync); connect(nativeMenuItem->action(), &QQuickAction::enabledChanged, nativeMenuItem, &QQuickNativeMenuItem::sync); connect(nativeMenuItem->action(), &QQuickAction::checkedChanged, nativeMenuItem, &QQuickNativeMenuItem::sync); connect(nativeMenuItem->action(), &QQuickAction::checkableChanged, nativeMenuItem, &QQuickNativeMenuItem::sync); break; case Type::SubMenu: nativeMenuItem->m_handle->setMenu(QQuickMenuPrivate::get( nativeMenuItem->subMenu())->handle.get()); // Handle programmatic changes in the Menu. connect(nativeMenuItem->subMenu(), &QQuickMenu::enabledChanged, nativeMenuItem, &QQuickNativeMenuItem::sync); connect(nativeMenuItem->subMenu(), &QQuickMenu::titleChanged, nativeMenuItem, &QQuickNativeMenuItem::sync); break; case Type::MenuItem: // Ensure that the MenuItem is clicked when the user clicks on a native menu item. connect(nativeMenuItem->m_handle.get(), &QPlatformMenuItem::activated, menuItem, [menuItem](){ // This changes the checked state, which we need when syncing but also to ensure that // the user can still use MenuItem's API even though they can't actually interact with it. menuItem->toggle(); // The same applies here: allow users to respond to the MenuItem's clicked signal. QQuickAbstractButtonPrivate::get(menuItem)->click(); }); // Handle programmatic changes in the MenuItem. connect(menuItem, &QQuickMenuItem::textChanged, nativeMenuItem, &QQuickNativeMenuItem::sync); connect(menuItem, &QQuickMenuItem::iconChanged, nativeMenuItem, &QQuickNativeMenuItem::sync); connect(menuItem, &QQuickMenuItem::enabledChanged, nativeMenuItem, &QQuickNativeMenuItem::sync); connect(menuItem, &QQuickMenuItem::checkedChanged, nativeMenuItem, &QQuickNativeMenuItem::sync); connect(menuItem, &QQuickMenuItem::checkableChanged, nativeMenuItem, &QQuickNativeMenuItem::sync); break; case Type::Separator: case Type::Unknown: break; } return nativeMenuItem; } QQuickNativeMenuItem::QQuickNativeMenuItem(QQuickMenu *parentMenu, QQuickItem *nonNativeItem, QQuickNativeMenuItem::Type type) : QObject(parentMenu) , m_parentMenu(parentMenu) , m_nonNativeItem(nonNativeItem) , m_type(type) { } QQuickNativeMenuItem::~QQuickNativeMenuItem() { qCDebug(lcNativeMenuItem) << "destroying" << this << debugText(); } QQuickAction *QQuickNativeMenuItem::action() const { return m_type == Type::Action ? qobject_cast(m_nonNativeItem)->action() : nullptr; } QQuickMenu *QQuickNativeMenuItem::subMenu() const { return m_type == Type::SubMenu ? qobject_cast(m_nonNativeItem)->subMenu() : nullptr; } QQuickMenuSeparator *QQuickNativeMenuItem::separator() const { return m_type == Type::Separator ? qobject_cast(m_nonNativeItem) : nullptr; } QPlatformMenuItem *QQuickNativeMenuItem::handle() const { return m_handle.get(); } void QQuickNativeMenuItem::sync() { if (m_type == Type::Unknown) return; if (m_syncing) return; QScopedValueRollback recursionGuard(m_syncing, true); const auto *action = this->action(); const auto *separator = this->separator(); auto *subMenu = this->subMenu(); auto *menuItem = qobject_cast(m_nonNativeItem); // Store the values in variables so that we can use it in the debug output below. const bool enabled = action ? action->isEnabled() : subMenu ? subMenu->isEnabled() : menuItem && menuItem->isEnabled(); m_handle->setEnabled(enabled); // m_handle->setVisible(isVisible()); const bool isSeparator = separator != nullptr; m_handle->setIsSeparator(isSeparator); const bool checkable = action ? action->isCheckable() : menuItem && menuItem->isCheckable(); m_handle->setCheckable(checkable); const bool checked = action ? action->isChecked() : menuItem && menuItem->isChecked(); m_handle->setChecked(checked); m_handle->setRole(QPlatformMenuItem::TextHeuristicRole); const QString text = action ? action->text() : subMenu ? subMenu->title() : menuItem ? menuItem->text() : QString(); m_handle->setText(text); // m_handle->setFont(m_font); // m_handle->setHasExclusiveGroup(m_group && m_group->isExclusive()); m_handle->setHasExclusiveGroup(false); const QQuickIcon icon = effectiveIcon(); const auto *menuPrivate = QQuickMenuPrivate::get(m_parentMenu); const auto *window = qGuiApp->topLevelWindows().first(); // We should reload the icon if the window's DPR has changed, regardless if its properties have changed. // We can't check for ItemDevicePixelRatioHasChanged in QQuickMenu::itemChange, // because that isn't sent when the menu isn't visible, and will never // be sent for native menus. We instead store lastDevicePixelRatio in QQuickMenu // (to avoid storing it for each menu item) and set it whenever it's opened. const bool dprChanged = !qFuzzyCompare(window->devicePixelRatio(), menuPrivate->lastDevicePixelRatio); if (!icon.isEmpty() && (icon != iconLoader()->icon() || dprChanged)) { // This will load the icon, which will call sync() recursively, hence the m_syncing check. reloadIcon(); } if (subMenu) { // Sync first as dynamically created menus may need to get the handle recreated. auto *subMenuPrivate = QQuickMenuPrivate::get(subMenu); subMenuPrivate->syncWithNativeMenu(); if (subMenuPrivate->handle) m_handle->setMenu(subMenuPrivate->handle.get()); } #if QT_CONFIG(shortcut) if (action) m_handle->setShortcut(action->shortcut()); #endif if (m_parentMenu) { auto *menuPrivate = QQuickMenuPrivate::get(m_parentMenu); if (menuPrivate->handle) menuPrivate->handle->syncMenuItem(m_handle.get()); } qCDebug(lcNativeMenuItem) << "sync called on" << debugText() << "handle" << m_handle.get() << "enabled:" << enabled << "isSeparator" << isSeparator << "checkable" << checkable << "checked" << checked << "text" << text; } QQuickIcon QQuickNativeMenuItem::effectiveIcon() const { if (const auto *action = this->action()) return action->icon(); if (const auto *subMenu = this->subMenu()) return subMenu->icon(); if (const auto *menuItem = qobject_cast(m_nonNativeItem)) return menuItem->icon(); return {}; } QQuickNativeIconLoader *QQuickNativeMenuItem::iconLoader() const { if (!m_iconLoader) { QQuickNativeMenuItem *that = const_cast(this); static int slot = staticMetaObject.indexOfSlot("updateIcon()"); m_iconLoader = new QQuickNativeIconLoader(slot, that); // Qt Labs Platform's QQuickMenuItem would call m_iconLoader->setEnabled(m_complete) here, // but since QQuickMenuPrivate::maybeCreateAndInsertNativeItem asserts that the menu's // completed loading, we can just set it to true. m_iconLoader->setEnabled(true); } return m_iconLoader; } void QQuickNativeMenuItem::reloadIcon() { iconLoader()->setIcon(effectiveIcon()); m_handle->setIcon(iconLoader()->toQIcon()); } void QQuickNativeMenuItem::updateIcon() { sync(); } void QQuickNativeMenuItem::addShortcut() { #if QT_CONFIG(shortcut) const auto *action = this->action(); const QKeySequence sequence = action ? action->shortcut() : QKeySequence(); if (!sequence.isEmpty() && 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; QKeySequence sequence; switch (m_type) { case Type::Action: sequence = action()->shortcut(); break; default: // TODO break; } QGuiApplicationPrivate::instance()->shortcutMap.removeShortcut(m_shortcutId, this, sequence); #endif } QString QQuickNativeMenuItem::debugText() const { switch (m_type) { case Type::Action: return QString::fromLatin1("Action(text = %1)").arg(action()->text()); case Type::SubMenu: return QString::fromLatin1("Sub-menu(title = %1)").arg(subMenu()->title()); case Type::MenuItem: return QString::fromLatin1("MenuItem(text = %1)").arg( qobject_cast(m_nonNativeItem)->text()); case Type::Separator: return QStringLiteral("Separator"); case Type::Unknown: return QStringLiteral("(Unknown)"); } Q_UNREACHABLE(); } QT_END_NAMESPACE #include "moc_qquicknativemenuitem_p.cpp"