diff options
Diffstat (limited to 'src/quicktemplates/qquickmenu.cpp')
-rw-r--r-- | src/quicktemplates/qquickmenu.cpp | 1574 |
1 files changed, 1574 insertions, 0 deletions
diff --git a/src/quicktemplates/qquickmenu.cpp b/src/quicktemplates/qquickmenu.cpp new file mode 100644 index 0000000000..707b115983 --- /dev/null +++ b/src/quicktemplates/qquickmenu.cpp @@ -0,0 +1,1574 @@ +// 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 "qquickmenu_p.h" +#include "qquickmenu_p_p.h" +#include "qquickmenuitem_p_p.h" +#include <private/qtquicktemplates2-config_p.h> +#if QT_CONFIG(quicktemplates2_container) +#include "qquickmenubaritem_p.h" +#include "qquickmenubar_p.h" +#endif +#include "qquickpopupitem_p_p.h" +#include "qquickpopuppositioner_p_p.h" +#include "qquickaction_p.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/private/qguiapplication_p.h> +#include <QtQml/qqmlcontext.h> +#include <QtQml/qqmlcomponent.h> +#include <QtQml/private/qqmlengine_p.h> +#include <QtQml/private/qv4scopedvalue_p.h> +#include <QtQml/private/qv4variantobject_p.h> +#include <QtQml/private/qv4qobjectwrapper_p.h> +#include <private/qqmlobjectmodel_p.h> +#include <QtQuick/private/qquickitem_p.h> +#include <QtQuick/private/qquickitemchangelistener_p.h> +#include <QtQuick/private/qquickevents_p_p.h> +#include <QtQuick/private/qquickwindow_p.h> + +QT_BEGIN_NAMESPACE + +// copied from qfusionstyle.cpp +static const int SUBMENU_DELAY = 225; + +/*! + \qmltype Menu + \inherits Popup +//! \instantiates QQuickMenu + \inqmlmodule QtQuick.Controls + \since 5.7 + \ingroup qtquickcontrols-menus + \ingroup qtquickcontrols-popups + \brief Menu popup that can be used as a context menu or popup menu. + + \image qtquickcontrols-menu.png + + Menu has two main use cases: + \list + \li Context menus; for example, a menu that is shown after right clicking + \li Popup menus; for example, a menu that is shown after clicking a button + \endlist + + When used as a context menu, the recommended way of opening the menu is to call + \l popup(). Unless a position is explicitly specified, the menu is positioned at + the mouse cursor on desktop platforms that have a mouse cursor available, and + otherwise centered over its parent item. + + \code + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: { + if (mouse.button === Qt.RightButton) + contextMenu.popup() + } + onPressAndHold: { + if (mouse.source === Qt.MouseEventNotSynthesized) + contextMenu.popup() + } + + Menu { + id: contextMenu + MenuItem { text: "Cut" } + MenuItem { text: "Copy" } + MenuItem { text: "Paste" } + } + } + \endcode + + When used as a popup menu, it is easiest to specify the position by specifying + the desired \l {Popup::}{x} and \l {Popup::}{y} coordinates using the respective + properties, and call \l {Popup::}{open()} to open the menu. + + \code + Button { + id: fileButton + text: "File" + onClicked: menu.open() + + Menu { + id: menu + y: fileButton.height + + MenuItem { + text: "New..." + } + MenuItem { + text: "Open..." + } + MenuItem { + text: "Save" + } + } + } + \endcode + + If the button should also close the menu when clicked, use the + \c Popup.CloseOnPressOutsideParent flag: + \code + onClicked: menu.visible = !menu.visible + + Menu { + // ... + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + \endcode + + Since QtQuick.Controls 2.3 (Qt 5.10), it is also possible to create sub-menus + and declare Action objects inside Menu: + + \code + Menu { + Action { text: "Cut" } + Action { text: "Copy" } + Action { text: "Paste" } + + MenuSeparator { } + + Menu { + title: "Find/Replace" + Action { text: "Find Next" } + Action { text: "Find Previous" } + Action { text: "Replace" } + } + } + \endcode + + Sub-menus are \l {cascade}{cascading} by default on desktop platforms + that have a mouse cursor available. Non-cascading menus are shown one + menu at a time, and centered over the parent menu. + + Typically, menu items are statically declared as children of the menu, but + Menu also provides API to \l {addItem}{add}, \l {insertItem}{insert}, + \l {moveItem}{move} and \l {removeItem}{remove} items dynamically. The + items in a menu can be accessed using \l itemAt() or + \l {Popup::}{contentChildren}. + + Although \l {MenuItem}{MenuItems} are most commonly used with Menu, it can + contain any type of item. + + \section1 Margins + + As it is inherited from Popup, Menu supports \l {Popup::}{margins}. By + default, all of the built-in styles specify \c 0 for Menu's margins to + ensure that the menu is kept within the bounds of the window. To allow a + menu to go outside of the window (to animate it moving into view, for + example), set the margins property to \c -1. + + \section1 Dynamically Generating Menu Items + + You can dynamically create menu items with \l Instantiator or + \l {Dynamic QML Object Creation from JavaScript} {dynamic object creation}. + + \section2 Using Instantiator + + 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 qtquickcontrols-menu-instantiator.qml menu + + \section2 Using Dynamic Object Creation + + You can also dynamically load a component from a QML file using + \l {QtQml::Qt::createComponent()} {Qt.createComponent()}. Once the component + is ready, you can call its \l {Component::createObject()} {createObject()} + method to create an instance of that component. + + \snippet qtquickcontrols-menu-createObject.qml createObject + + \sa {Customizing Menu}, MenuItem, {Menu Controls}, {Popup Controls}, + {Dynamic QML Object Creation from JavaScript} +*/ + +/*! + \qmlproperty bool QtQuick.Controls::Menu::focus + + This property holds whether the popup wants focus. + + When the popup actually receives focus, \l{Popup::}{activeFocus} + will be \c true. For more information, see + \l {Keyboard Focus in Qt Quick}. + + The default value is \c true. + + \sa {Popup::}{activeFocus} +*/ + +static const QQuickPopup::ClosePolicy cascadingSubMenuClosePolicy = QQuickPopup::CloseOnEscape | QQuickPopup::CloseOnPressOutsideParent; + +static bool shouldCascade() +{ +#if QT_CONFIG(cursor) + return QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::MultipleWindows); +#else + return false; +#endif +} + +class QQuickMenuPositioner : public QQuickPopupPositioner +{ +public: + QQuickMenuPositioner(QQuickMenu *menu) : QQuickPopupPositioner(menu) { } + + void reposition() override; +}; + +QQuickMenuPrivate::QQuickMenuPrivate() +{ + cascade = shouldCascade(); +} + +void QQuickMenuPrivate::init() +{ + Q_Q(QQuickMenu); + contentModel = new QQmlObjectModel(q); +} + +QQuickItem *QQuickMenuPrivate::itemAt(int index) const +{ + return qobject_cast<QQuickItem *>(contentModel->get(index)); +} + +void QQuickMenuPrivate::insertItem(int index, QQuickItem *item) +{ + contentData.append(item); + item->setParentItem(contentItem); + QQuickItemPrivate::get(item)->setCulled(true); // QTBUG-53262 + if (complete) + resizeItem(item); + QQuickItemPrivate::get(item)->addItemChangeListener(this, QQuickItemPrivate::Destroyed | QQuickItemPrivate::Parent); + QQuickItemPrivate::get(item)->updateOrAddGeometryChangeListener(this, QQuickGeometryChange::Width); + contentModel->insert(index, item); + + QQuickMenuItem *menuItem = qobject_cast<QQuickMenuItem *>(item); + if (menuItem) { + Q_Q(QQuickMenu); + QQuickMenuItemPrivate::get(menuItem)->setMenu(q); + if (QQuickMenu *subMenu = menuItem->subMenu()) + QQuickMenuPrivate::get(subMenu)->setParentMenu(q); + QObjectPrivate::connect(menuItem, &QQuickMenuItem::triggered, this, &QQuickMenuPrivate::onItemTriggered); + QObjectPrivate::connect(menuItem, &QQuickItem::activeFocusChanged, this, &QQuickMenuPrivate::onItemActiveFocusChanged); + QObjectPrivate::connect(menuItem, &QQuickControl::hoveredChanged, this, &QQuickMenuPrivate::onItemHovered); + } +} + +void QQuickMenuPrivate::moveItem(int from, int to) +{ + contentModel->move(from, to); +} + +void QQuickMenuPrivate::removeItem(int index, QQuickItem *item) +{ + contentData.removeOne(item); + + QQuickItemPrivate::get(item)->removeItemChangeListener(this, QQuickItemPrivate::Destroyed | QQuickItemPrivate::Parent); + QQuickItemPrivate::get(item)->removeItemChangeListener(this, QQuickItemPrivate::Geometry); + item->setParentItem(nullptr); + contentModel->remove(index); + + QQuickMenuItem *menuItem = qobject_cast<QQuickMenuItem *>(item); + if (menuItem) { + QQuickMenuItemPrivate::get(menuItem)->setMenu(nullptr); + if (QQuickMenu *subMenu = menuItem->subMenu()) + QQuickMenuPrivate::get(subMenu)->setParentMenu(nullptr); + QObjectPrivate::disconnect(menuItem, &QQuickMenuItem::triggered, this, &QQuickMenuPrivate::onItemTriggered); + QObjectPrivate::disconnect(menuItem, &QQuickItem::activeFocusChanged, this, &QQuickMenuPrivate::onItemActiveFocusChanged); + QObjectPrivate::disconnect(menuItem, &QQuickControl::hoveredChanged, this, &QQuickMenuPrivate::onItemHovered); + } +} + +QQuickItem *QQuickMenuPrivate::beginCreateItem() +{ + Q_Q(QQuickMenu); + if (!delegate) + return nullptr; + + QQmlContext *context = delegate->creationContext(); + if (!context) + context = qmlContext(q); + + QObject *object = delegate->beginCreate(context); + QQuickItem *item = qobject_cast<QQuickItem *>(object); + if (!item) + delete object; + else + QQml_setParent_noEvent(item, q); + + return item; +} + +void QQuickMenuPrivate::completeCreateItem() +{ + if (!delegate) + return; + + delegate->completeCreate(); +} + +QQuickItem *QQuickMenuPrivate::createItem(QQuickMenu *menu) +{ + QQuickItem *item = beginCreateItem(); + if (QQuickMenuItem *menuItem = qobject_cast<QQuickMenuItem *>(item)) + QQuickMenuItemPrivate::get(menuItem)->setSubMenu(menu); + completeCreateItem(); + return item; +} + +QQuickItem *QQuickMenuPrivate::createItem(QQuickAction *action) +{ + QQuickItem *item = beginCreateItem(); + if (QQuickAbstractButton *button = qobject_cast<QQuickAbstractButton *>(item)) + button->setAction(action); + completeCreateItem(); + return item; +} + +void QQuickMenuPrivate::resizeItem(QQuickItem *item) +{ + if (!item || !contentItem) + return; + + QQuickItemPrivate *p = QQuickItemPrivate::get(item); + if (!p->widthValid()) { + item->setWidth(contentItem->width()); + p->widthValidFlag = false; + } +} + +void QQuickMenuPrivate::resizeItems() +{ + if (!contentModel) + return; + + for (int i = 0; i < contentModel->count(); ++i) + resizeItem(itemAt(i)); +} + +void QQuickMenuPrivate::itemChildAdded(QQuickItem *, QQuickItem *child) +{ + // add dynamically reparented items (eg. by a Repeater) + if (!QQuickItemPrivate::get(child)->isTransparentForPositioner() && !contentData.contains(child)) + insertItem(contentModel->count(), child); +} + +void QQuickMenuPrivate::itemParentChanged(QQuickItem *item, QQuickItem *parent) +{ + // remove dynamically unparented items (eg. by a Repeater) + if (!parent) + removeItem(contentModel->indexOf(item, nullptr), item); +} + +void QQuickMenuPrivate::itemSiblingOrderChanged(QQuickItem *) +{ + // reorder the restacked items (eg. by a Repeater) + Q_Q(QQuickMenu); + QList<QQuickItem *> siblings = contentItem->childItems(); + + int to = 0; + for (int i = 0; i < siblings.size(); ++i) { + QQuickItem* sibling = siblings.at(i); + if (QQuickItemPrivate::get(sibling)->isTransparentForPositioner()) + continue; + int index = contentModel->indexOf(sibling, nullptr); + q->moveItem(index, to++); + } +} + +void QQuickMenuPrivate::itemDestroyed(QQuickItem *item) +{ + QQuickPopupPrivate::itemDestroyed(item); + int index = contentModel->indexOf(item, nullptr); + if (index != -1) + removeItem(index, item); +} + +void QQuickMenuPrivate::itemGeometryChanged(QQuickItem *item, QQuickGeometryChange, const QRectF &) +{ + if (!complete) + return; + + if (item == contentItem) { + // The contentItem's geometry changed, so resize any items + // that don't have explicit widths set so that they fill the width of the menu. + resizeItems(); + } else { + // The geometry of an item in the menu changed. If the item + // doesn't have an explicit width set, make it fill the width of the menu. + resizeItem(item); + } +} + +QQuickPopupPositioner *QQuickMenuPrivate::getPositioner() +{ + Q_Q(QQuickMenu); + if (!positioner) + positioner = new QQuickMenuPositioner(q); + return positioner; +} + +void QQuickMenuPositioner::reposition() +{ + QQuickMenu *menu = static_cast<QQuickMenu *>(popup()); + QQuickMenuPrivate *p = QQuickMenuPrivate::get(menu); + if (p->parentMenu) { + if (p->cascade) { + if (p->popupItem->isMirrored()) + menu->setPosition(QPointF(-menu->width() - p->parentMenu->leftPadding() + menu->overlap(), -menu->topPadding())); + else if (p->parentItem) + menu->setPosition(QPointF(p->parentItem->width() + p->parentMenu->rightPadding() - menu->overlap(), -menu->topPadding())); + } else { + menu->setPosition(QPointF(p->parentMenu->x() + (p->parentMenu->width() - menu->width()) / 2, + p->parentMenu->y() + (p->parentMenu->height() - menu->height()) / 2)); + } + } + QQuickPopupPositioner::reposition(); +} + +bool QQuickMenuPrivate::prepareEnterTransition() +{ + Q_Q(QQuickMenu); + if (parentMenu && !cascade) + parentMenu->close(); + + // If a cascading sub-menu doesn't have enough space to open on + // the right, it flips on the other side of the parent menu. + allowHorizontalFlip = cascade && parentMenu; + + if (!QQuickPopupPrivate::prepareEnterTransition()) + return false; + + if (!hasClosePolicy) { + if (cascade && parentMenu) + closePolicy = cascadingSubMenuClosePolicy; + else + q->resetClosePolicy(); + } + return true; +} + +bool QQuickMenuPrivate::prepareExitTransition() +{ + if (!QQuickPopupPrivate::prepareExitTransition()) + return false; + + stopHoverTimer(); + + QQuickMenu *subMenu = currentSubMenu(); + while (subMenu) { + QPointer<QQuickMenuItem> currentSubMenuItem = QQuickMenuPrivate::get(subMenu)->currentItem; + subMenu->close(); + subMenu = currentSubMenuItem ? currentSubMenuItem->subMenu() : nullptr; + } + return true; +} + +bool QQuickMenuPrivate::blockInput(QQuickItem *item, const QPointF &point) const +{ + // keep the parent menu open when a cascading sub-menu (this menu) is interacted with + return (cascade && parentMenu && contains(point)) || QQuickPopupPrivate::blockInput(item, point); +} + +void QQuickMenuPrivate::onItemHovered() +{ + Q_Q(QQuickMenu); + QQuickAbstractButton *button = qobject_cast<QQuickAbstractButton *>(q->sender()); + if (!button || !button->isHovered() || !button->isEnabled() || QQuickAbstractButtonPrivate::get(button)->touchId != -1) + return; + + QQuickMenuItem *oldCurrentItem = currentItem; + + int index = contentModel->indexOf(button, nullptr); + if (index != -1) { + setCurrentIndex(index, Qt::OtherFocusReason); + if (oldCurrentItem != currentItem) { + if (oldCurrentItem) { + QQuickMenu *subMenu = oldCurrentItem->subMenu(); + if (subMenu) + subMenu->close(); + } + if (currentItem) { + QQuickMenu *subMenu = currentItem->menu(); + if (subMenu && subMenu->cascade()) + startHoverTimer(); + } + } + } +} + +void QQuickMenuPrivate::onItemTriggered() +{ + Q_Q(QQuickMenu); + QQuickMenuItem *item = qobject_cast<QQuickMenuItem *>(q->sender()); + if (!item) + return; + + if (QQuickMenu *subMenu = item->subMenu()) { + auto subMenuPrivate = QQuickMenuPrivate::get(subMenu); + subMenu->popup(subMenuPrivate->firstEnabledMenuItem()); + } else { + q->dismiss(); + } +} + +void QQuickMenuPrivate::onItemActiveFocusChanged() +{ + Q_Q(QQuickMenu); + QQuickItem *item = qobject_cast<QQuickItem*>(q->sender()); + if (!item->hasActiveFocus()) + return; + + int indexOfItem = contentModel->indexOf(item, nullptr); + QQuickControl *control = qobject_cast<QQuickControl *>(item); + setCurrentIndex(indexOfItem, control ? control->focusReason() : Qt::OtherFocusReason); +} + +QQuickMenu *QQuickMenuPrivate::currentSubMenu() const +{ + if (!currentItem) + return nullptr; + + return currentItem->subMenu(); +} + +void QQuickMenuPrivate::setParentMenu(QQuickMenu *parent) +{ + Q_Q(QQuickMenu); + if (parentMenu == parent) + return; + + if (parentMenu) { + QObject::disconnect(parentMenu.data(), &QQuickMenu::cascadeChanged, q, &QQuickMenu::setCascade); + disconnect(parentMenu.data(), &QQuickMenu::parentChanged, this, &QQuickMenuPrivate::resolveParentItem); + } + if (parent) { + QObject::connect(parent, &QQuickMenu::cascadeChanged, q, &QQuickMenu::setCascade); + connect(parent, &QQuickMenu::parentChanged, this, &QQuickMenuPrivate::resolveParentItem); + } + + parentMenu = parent; + q->resetCascade(); + resolveParentItem(); +} + +static QQuickItem *findParentMenuItem(QQuickMenu *subMenu) +{ + QQuickMenu *menu = QQuickMenuPrivate::get(subMenu)->parentMenu; + for (int i = 0; i < QQuickMenuPrivate::get(menu)->contentModel->count(); ++i) { + QQuickMenuItem *item = qobject_cast<QQuickMenuItem *>(menu->itemAt(i)); + if (item && item->subMenu() == subMenu) + return item; + } + return nullptr; +} + +void QQuickMenuPrivate::resolveParentItem() +{ + Q_Q(QQuickMenu); + if (!parentMenu) + q->resetParentItem(); + else if (!cascade) + q->setParentItem(parentMenu->parentItem()); + else + q->setParentItem(findParentMenuItem(q)); +} + +void QQuickMenuPrivate::propagateKeyEvent(QKeyEvent *event) +{ + if (QQuickMenuItem *menuItem = qobject_cast<QQuickMenuItem *>(parentItem)) { + if (QQuickMenu *menu = menuItem->menu()) + QQuickMenuPrivate::get(menu)->propagateKeyEvent(event); +#if QT_CONFIG(quicktemplates2_container) + } else if (QQuickMenuBarItem *menuBarItem = qobject_cast<QQuickMenuBarItem *>(parentItem)) { + if (QQuickMenuBar *menuBar = menuBarItem->menuBar()) { + event->accept(); + QCoreApplication::sendEvent(menuBar, event); + } +#endif + } +} + +void QQuickMenuPrivate::startHoverTimer() +{ + Q_Q(QQuickMenu); + stopHoverTimer(); + hoverTimer = q->startTimer(SUBMENU_DELAY); +} + +void QQuickMenuPrivate::stopHoverTimer() +{ + Q_Q(QQuickMenu); + if (!hoverTimer) + return; + + q->killTimer(hoverTimer); + hoverTimer = 0; +} + +void QQuickMenuPrivate::setCurrentIndex(int index, Qt::FocusReason reason) +{ + Q_Q(QQuickMenu); + if (currentIndex == index) + return; + + QQuickMenuItem *newCurrentItem = qobject_cast<QQuickMenuItem *>(itemAt(index)); + if (currentItem != newCurrentItem) { + stopHoverTimer(); + if (currentItem) { + currentItem->setHighlighted(false); + if (!newCurrentItem && window) { + QQuickItem *focusItem = QQuickItemPrivate::get(contentItem)->subFocusItem; + if (focusItem) + QQuickWindowPrivate::get(window)->clearFocusInScope(contentItem, focusItem, Qt::OtherFocusReason); + } + } + if (newCurrentItem) { + newCurrentItem->setHighlighted(true); + newCurrentItem->forceActiveFocus(reason); + } + currentItem = newCurrentItem; + } + + currentIndex = index; + emit q->currentIndexChanged(); +} + +bool QQuickMenuPrivate::activateNextItem() +{ + int index = currentIndex; + int count = contentModel->count(); + while (++index < count) { + QQuickItem *item = itemAt(index); + if (!item || !item->activeFocusOnTab() || !item->isEnabled()) + continue; + setCurrentIndex(index, Qt::TabFocusReason); + return true; + } + return false; +} + +bool QQuickMenuPrivate::activatePreviousItem() +{ + int index = currentIndex; + while (--index >= 0) { + QQuickItem *item = itemAt(index); + if (!item || !item->activeFocusOnTab() || !item->isEnabled()) + continue; + setCurrentIndex(index, Qt::BacktabFocusReason); + return true; + } + return false; +} + +QQuickMenuItem *QQuickMenuPrivate::firstEnabledMenuItem() const +{ + for (int i = 0; i < contentModel->count(); ++i) { + QQuickItem *item = itemAt(i); + if (!item || !item->isEnabled()) + continue; + + QQuickMenuItem *menuItem = qobject_cast<QQuickMenuItem *>(item); + if (!menuItem) + continue; + + return menuItem; + } + return nullptr; +} + +void QQuickMenuPrivate::contentData_append(QQmlListProperty<QObject> *prop, QObject *obj) +{ + QQuickMenu *q = qobject_cast<QQuickMenu *>(prop->object); + QQuickMenuPrivate *p = QQuickMenuPrivate::get(q); + + 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); + } + + if (item) { + if (QQuickItemPrivate::get(item)->isTransparentForPositioner()) { + QQuickItemPrivate::get(item)->addItemChangeListener(p, QQuickItemPrivate::SiblingOrder); + item->setParentItem(p->contentItem); + } else if (p->contentModel->indexOf(item, nullptr) == -1) { + q->addItem(item); + } + } else { + p->contentData.append(obj); + } +} + +qsizetype QQuickMenuPrivate::contentData_count(QQmlListProperty<QObject> *prop) +{ + QQuickMenu *q = static_cast<QQuickMenu *>(prop->object); + return QQuickMenuPrivate::get(q)->contentData.size(); +} + +QObject *QQuickMenuPrivate::contentData_at(QQmlListProperty<QObject> *prop, qsizetype index) +{ + QQuickMenu *q = static_cast<QQuickMenu *>(prop->object); + return QQuickMenuPrivate::get(q)->contentData.value(index); +} + +QPalette QQuickMenuPrivate::defaultPalette() const +{ + return QQuickTheme::palette(QQuickTheme::Menu); +} + +void QQuickMenuPrivate::contentData_clear(QQmlListProperty<QObject> *prop) +{ + QQuickMenu *q = static_cast<QQuickMenu *>(prop->object); + QQuickMenuPrivate::get(q)->contentData.clear(); +} + +QQuickMenu::QQuickMenu(QObject *parent) + : QQuickPopup(*(new QQuickMenuPrivate), parent) +{ + Q_D(QQuickMenu); + setFocus(true); + d->init(); + connect(d->contentModel, &QQmlObjectModel::countChanged, this, &QQuickMenu::countChanged); +} + +QQuickMenu::~QQuickMenu() +{ + Q_D(QQuickMenu); + // We have to do this to ensure that the change listeners are removed. + // It's too late to do this in ~QQuickMenuPrivate, as contentModel has already + // been destroyed before that is called. + while (d->contentModel->count() > 0) + d->removeItem(0, d->itemAt(0)); + + if (d->contentItem) { + QQuickItemPrivate::get(d->contentItem)->removeItemChangeListener(d, QQuickItemPrivate::Children); + QQuickItemPrivate::get(d->contentItem)->removeItemChangeListener(d, QQuickItemPrivate::Geometry); + + const auto children = d->contentItem->childItems(); + for (QQuickItem *child : std::as_const(children)) + QQuickItemPrivate::get(child)->removeItemChangeListener(d, QQuickItemPrivate::SiblingOrder); + } +} + +/*! + \qmlmethod Item QtQuick.Controls::Menu::itemAt(int index) + + Returns the item at \a index, or \c null if it does not exist. +*/ +QQuickItem *QQuickMenu::itemAt(int index) const +{ + Q_D(const QQuickMenu); + return d->itemAt(index); +} + +/*! + \qmlmethod void QtQuick.Controls::Menu::addItem(Item item) + + Adds \a item to the end of the list of items. +*/ +void QQuickMenu::addItem(QQuickItem *item) +{ + Q_D(QQuickMenu); + insertItem(d->contentModel->count(), item); +} + +/*! + \qmlmethod void QtQuick.Controls::Menu::insertItem(int index, Item item) + + Inserts \a item at \a index. +*/ +void QQuickMenu::insertItem(int index, QQuickItem *item) +{ + Q_D(QQuickMenu); + if (!item) + return; + const int count = d->contentModel->count(); + if (index < 0 || index > count) + index = count; + + int oldIndex = d->contentModel->indexOf(item, nullptr); + if (oldIndex != -1) { + if (oldIndex < index) + --index; + if (oldIndex != index) + d->moveItem(oldIndex, index); + } else { + d->insertItem(index, item); + } +} + +/*! + \qmlmethod void QtQuick.Controls::Menu::moveItem(int from, int to) + + Moves an item \a from one index \a to another. +*/ +void QQuickMenu::moveItem(int from, int to) +{ + Q_D(QQuickMenu); + const int count = d->contentModel->count(); + if (from < 0 || from > count - 1) + return; + if (to < 0 || to > count - 1) + to = count - 1; + + if (from != to) + d->moveItem(from, to); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod void QtQuick.Controls::Menu::removeItem(Item item) + + Removes and destroys the specified \a item. +*/ +void QQuickMenu::removeItem(QQuickItem *item) +{ + Q_D(QQuickMenu); + if (!item) + return; + + const int index = d->contentModel->indexOf(item, nullptr); + if (index == -1) + return; + + d->removeItem(index, item); + item->deleteLater(); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod MenuItem QtQuick.Controls::Menu::takeItem(int index) + + Removes and returns the item at \a index. + + \note The ownership of the item is transferred to the caller. +*/ +QQuickItem *QQuickMenu::takeItem(int index) +{ + Q_D(QQuickMenu); + const int count = d->contentModel->count(); + if (index < 0 || index >= count) + return nullptr; + + QQuickItem *item = itemAt(index); + if (item) + d->removeItem(index, item); + return item; +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod Menu QtQuick.Controls::Menu::menuAt(int index) + + Returns the sub-menu at \a index, or \c null if the index is not valid or + there is no sub-menu at the specified index. +*/ +QQuickMenu *QQuickMenu::menuAt(int index) const +{ + Q_D(const QQuickMenu); + QQuickMenuItem *item = qobject_cast<QQuickMenuItem *>(d->itemAt(index)); + if (!item) + return nullptr; + + return item->subMenu(); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod void QtQuick.Controls::Menu::addMenu(Menu menu) + + Adds \a menu as a sub-menu to the end of this menu. +*/ +void QQuickMenu::addMenu(QQuickMenu *menu) +{ + Q_D(QQuickMenu); + insertMenu(d->contentModel->count(), menu); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod void QtQuick.Controls::Menu::insertMenu(int index, Menu menu) + + Inserts \a menu as a sub-menu at \a index. The index is within all items in the menu. +*/ +void QQuickMenu::insertMenu(int index, QQuickMenu *menu) +{ + Q_D(QQuickMenu); + if (!menu) + return; + + insertItem(index, d->createItem(menu)); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod void QtQuick.Controls::Menu::removeMenu(Menu menu) + + Removes and destroys the specified \a menu. +*/ +void QQuickMenu::removeMenu(QQuickMenu *menu) +{ + Q_D(QQuickMenu); + 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; + + removeItem(item); + break; + } + + menu->deleteLater(); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod Menu QtQuick.Controls::Menu::takeMenu(int index) + + Removes and returns the menu at \a index. The index is within all items in the menu. + + \note The ownership of the menu is transferred to the caller. +*/ +QQuickMenu *QQuickMenu::takeMenu(int index) +{ + Q_D(QQuickMenu); + QQuickMenuItem *item = qobject_cast<QQuickMenuItem *>(d->itemAt(index)); + if (!item) + return nullptr; + + QQuickMenu *subMenu = item->subMenu(); + if (!subMenu) + return nullptr; + + d->removeItem(index, item); + item->deleteLater(); + return subMenu; +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod Action QtQuick.Controls::Menu::actionAt(int index) + + Returns the action at \a index, or \c null if the index is not valid or + there is no action at the specified index. +*/ +QQuickAction *QQuickMenu::actionAt(int index) const +{ + Q_D(const QQuickMenu); + QQuickAbstractButton *item = qobject_cast<QQuickAbstractButton *>(d->itemAt(index)); + if (!item) + return nullptr; + + return item->action(); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod void QtQuick.Controls::Menu::addAction(Action action) + + Adds \a action to the end of this menu. +*/ +void QQuickMenu::addAction(QQuickAction *action) +{ + Q_D(QQuickMenu); + insertAction(d->contentModel->count(), action); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod void QtQuick.Controls::Menu::insertAction(int index, Action action) + + Inserts \a action at \a index. The index is within all items in the menu. +*/ +void QQuickMenu::insertAction(int index, QQuickAction *action) +{ + Q_D(QQuickMenu); + if (!action) + return; + + insertItem(index, d->createItem(action)); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod void QtQuick.Controls::Menu::removeAction(Action action) + + Removes and destroys the specified \a action. +*/ +void QQuickMenu::removeAction(QQuickAction *action) +{ + Q_D(QQuickMenu); + 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; + + removeItem(item); + break; + } + + action->deleteLater(); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod Action QtQuick.Controls::Menu::takeAction(int index) + + Removes and returns the action at \a index. The index is within all items in the menu. + + \note The ownership of the action is transferred to the caller. +*/ +QQuickAction *QQuickMenu::takeAction(int index) +{ + Q_D(QQuickMenu); + QQuickMenuItem *item = qobject_cast<QQuickMenuItem *>(d->itemAt(index)); + if (!item) + return nullptr; + + QQuickAction *action = item->action(); + if (!action) + return nullptr; + + d->removeItem(index, item); + item->deleteLater(); + return action; +} + +/*! + \qmlproperty model QtQuick.Controls::Menu::contentModel + \readonly + + This property holds the model used to display menu items. + + The content model is provided for visualization purposes. It can be assigned + as a model to a content item that presents the contents of the menu. + + \code + Menu { + id: menu + contentItem: ListView { + model: menu.contentModel + } + } + \endcode + + The model allows menu items to be statically declared as children of the + menu. +*/ +QVariant QQuickMenu::contentModel() const +{ + Q_D(const QQuickMenu); + return QVariant::fromValue(d->contentModel); +} + +/*! + \qmlproperty list<QtObject> QtQuick.Controls::Menu::contentData + \qmldefault + + This property holds the list of content data. + + The list contains all objects that have been declared in QML as children + of the menu, and also items that have been dynamically added or + inserted using the \l addItem() and \l insertItem() methods, respectively. + + \note Unlike \c contentChildren, \c contentData does include non-visual QML + objects. It is not re-ordered when items are inserted or moved. + + \sa Item::data, {Popup::}{contentChildren} +*/ +QQmlListProperty<QObject> QQuickMenu::contentData() +{ + Q_D(QQuickMenu); + if (!d->contentItem) + QQuickControlPrivate::get(d->popupItem)->executeContentItem(); + return QQmlListProperty<QObject>(this, nullptr, + QQuickMenuPrivate::contentData_append, + QQuickMenuPrivate::contentData_count, + QQuickMenuPrivate::contentData_at, + QQuickMenuPrivate::contentData_clear); +} + +/*! + \qmlproperty string QtQuick.Controls::Menu::title + + This property holds the title for the menu. + + The title of a menu is often displayed in the text of a menu item when the + menu is a submenu, and in the text of a tool button when it is in a + menubar. +*/ +QString QQuickMenu::title() const +{ + Q_D(const QQuickMenu); + return d->title; +} + +void QQuickMenu::setTitle(const QString &title) +{ + Q_D(QQuickMenu); + if (title == d->title) + return; + d->title = title; + emit titleChanged(title); +} + +/*! + \qmlproperty string QtQuick.Controls::Menu::icon.name + \qmlproperty url QtQuick.Controls::Menu::icon.source + \qmlproperty int QtQuick.Controls::Menu::icon.width + \qmlproperty int QtQuick.Controls::Menu::icon.height + \qmlproperty color QtQuick.Controls::Menu::icon.color + \qmlproperty bool QtQuick.Controls::Menu::icon.cache + + This property group was added in QtQuick.Controls 6.5. + + \include qquickicon.qdocinc grouped-properties + + \sa AbstractButton::text, AbstractButton::display, {Icons in Qt Quick Controls} +*/ + +QQuickIcon QQuickMenu::icon() const +{ + Q_D(const QQuickMenu); + return d->icon; +} + +void QQuickMenu::setIcon(const QQuickIcon &icon) +{ + Q_D(QQuickMenu); + if (icon == d->icon) + return; + d->icon = icon; + d->icon.ensureRelativeSourceResolved(this); + emit iconChanged(icon); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlproperty bool QtQuick.Controls::Menu::cascade + + This property holds whether the menu cascades its sub-menus. + + The default value is platform-specific. Menus are cascading by default on + desktop platforms that have a mouse cursor available. Non-cascading menus + are shown one menu at a time, and centered over the parent menu. + + \note Changing the value of the property has no effect while the menu is open. + + \sa overlap +*/ +bool QQuickMenu::cascade() const +{ + Q_D(const QQuickMenu); + return d->cascade; +} + +void QQuickMenu::setCascade(bool cascade) +{ + Q_D(QQuickMenu); + if (d->cascade == cascade) + return; + d->cascade = cascade; + if (d->parentMenu) + d->resolveParentItem(); + emit cascadeChanged(cascade); +} + +void QQuickMenu::resetCascade() +{ + Q_D(QQuickMenu); + if (d->parentMenu) + setCascade(d->parentMenu->cascade()); + else + setCascade(shouldCascade()); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlproperty real QtQuick.Controls::Menu::overlap + + This property holds the amount of pixels by which the menu horizontally overlaps its parent menu. + + The property only has effect when the menu is used as a cascading sub-menu. + + The default value is style-specific. + + \note Changing the value of the property has no effect while the menu is open. + + \sa cascade +*/ +qreal QQuickMenu::overlap() const +{ + Q_D(const QQuickMenu); + return d->overlap; +} + +void QQuickMenu::setOverlap(qreal overlap) +{ + Q_D(QQuickMenu); + if (d->overlap == overlap) + return; + d->overlap = overlap; + emit overlapChanged(); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlproperty Component QtQuick.Controls::Menu::delegate + + This property holds the component that is used to create items + to present actions. + + \code + Menu { + Action { text: "Cut" } + Action { text: "Copy" } + Action { text: "Paste" } + } + \endcode + + \sa Action +*/ +QQmlComponent *QQuickMenu::delegate() const +{ + Q_D(const QQuickMenu); + return d->delegate; +} + +void QQuickMenu::setDelegate(QQmlComponent *delegate) +{ + Q_D(QQuickMenu); + if (d->delegate == delegate) + return; + + d->delegate = delegate; + emit delegateChanged(); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlproperty int QtQuick.Controls::Menu::currentIndex + + This property holds the index of the currently highlighted item. + + Menu items can be highlighted by mouse hover or keyboard navigation. + + \sa MenuItem::highlighted +*/ +int QQuickMenu::currentIndex() const +{ + Q_D(const QQuickMenu); + return d->currentIndex; +} + +void QQuickMenu::setCurrentIndex(int index) +{ + Q_D(QQuickMenu); + d->setCurrentIndex(index, Qt::OtherFocusReason); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlproperty int QtQuick.Controls::Menu::count + \readonly + + This property holds the number of items. +*/ +int QQuickMenu::count() const +{ + Q_D(const QQuickMenu); + return d->contentModel->count(); +} + +void QQuickMenu::popup(QQuickItem *menuItem) +{ + Q_D(QQuickMenu); + // No position has been explicitly specified, so position the menu at the mouse cursor + // on desktop platforms that have a mouse cursor available and support multiple windows. + QQmlNullableValue<QPointF> pos; +#if QT_CONFIG(cursor) + if (d->parentItem && QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::MultipleWindows)) + pos = d->parentItem->mapFromGlobal(QCursor::pos()); +#endif + + // As a fallback, center the menu over its parent item. + if (!pos.isValid() && d->parentItem) + pos = QPointF((d->parentItem->width() - width()) / 2, (d->parentItem->height() - height()) / 2); + + popup(pos.isValid() ? pos.value() : QPointF(), menuItem); +} + +void QQuickMenu::popup(const QPointF &pos, QQuickItem *menuItem) +{ + Q_D(QQuickMenu); + qreal offset = 0; +#if QT_CONFIG(cursor) + if (menuItem) + offset = d->popupItem->mapFromItem(menuItem, QPointF(0, 0)).y(); +#endif + setPosition(pos - QPointF(0, offset)); + + if (menuItem) + d->setCurrentIndex(d->contentModel->indexOf(menuItem, nullptr), Qt::PopupFocusReason); + open(); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod void QtQuick.Controls::Menu::popup(MenuItem item = null) + \qmlmethod void QtQuick.Controls::Menu::popup(Item parent, MenuItem item = null) + + Opens the menu at the mouse cursor on desktop platforms that have a mouse cursor + available, and otherwise centers the menu over its \a parent item. + + The menu can be optionally aligned to a specific menu \a item. + + \sa Popup::open() +*/ + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod void QtQuick.Controls::Menu::popup(point pos, MenuItem item = null) + \qmlmethod void QtQuick.Controls::Menu::popup(Item parent, point pos, MenuItem item = null) + + Opens the menu at the specified position \a pos in the popups coordinate system, + that is, a coordinate relative to its \a parent item. + + The menu can be optionally aligned to a specific menu \a item. + + \sa Popup::open() +*/ + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod void QtQuick.Controls::Menu::popup(real x, real y, MenuItem item = null) + \qmlmethod void QtQuick.Controls::Menu::popup(Item parent, real x, real y, MenuItem item = null) + + Opens the menu at the specified position \a x, \a y in the popups coordinate system, + that is, a coordinate relative to its \a parent item. + + The menu can be optionally aligned to a specific menu \a item. + + \sa dismiss(), Popup::open() +*/ +void QQuickMenu::popup(QQmlV4FunctionPtr args) +{ + Q_D(QQuickMenu); + const int len = args->length(); + if (len > 4) { + args->v4engine()->throwTypeError(); + return; + } + + QV4::ExecutionEngine *v4 = args->v4engine(); + QV4::Scope scope(v4); + + QQmlNullableValue<QPointF> pos; + QQuickItem *menuItem = nullptr; + QQuickItem *parentItem = nullptr; + + if (len > 0) { + // Item parent + QV4::ScopedValue firstArg(scope, (*args)[0]); + if (const QV4::QObjectWrapper *obj = firstArg->as<QV4::QObjectWrapper>()) { + QQuickItem *item = qobject_cast<QQuickItem *>(obj->object()); + if (item && !d->popupItem->isAncestorOf(item)) + parentItem = item; + } else if (firstArg->isUndefined()) { + resetParentItem(); + parentItem = d->parentItem; + } + + // MenuItem item + QV4::ScopedValue lastArg(scope, (*args)[len - 1]); + if (const QV4::QObjectWrapper *obj = lastArg->as<QV4::QObjectWrapper>()) { + QQuickItem *item = qobject_cast<QQuickItem *>(obj->object()); + if (item && d->popupItem->isAncestorOf(item)) + menuItem = item; + } + } + + if (len >= 3 || (!parentItem && len >= 2)) { + // real x, real y + QV4::ScopedValue xArg(scope, (*args)[parentItem ? 1 : 0]); + QV4::ScopedValue yArg(scope, (*args)[parentItem ? 2 : 1]); + if (xArg->isNumber() && yArg->isNumber()) + pos = QPointF(xArg->asDouble(), yArg->asDouble()); + } + + if (!pos.isValid() && (len >= 2 || (!parentItem && len >= 1))) { + // point pos + QV4::ScopedValue posArg(scope, (*args)[parentItem ? 1 : 0]); + const QVariant var = QV4::ExecutionEngine::toVariant(posArg, QMetaType {}); + if (var.userType() == QMetaType::QPointF) + pos = var.toPointF(); + } + + if (parentItem) + setParentItem(parentItem); + + if (pos.isValid()) + popup(pos, menuItem); + else + popup(menuItem); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlmethod void QtQuick.Controls::Menu::dismiss() + + Closes all menus in the hierarchy that this menu belongs to. + + \note Unlike \l {Popup::}{close()} that only closes a menu and its sub-menus, + \c dismiss() closes the whole hierarchy of menus, including the parent menus. + In practice, \c close() is suitable e.g. for implementing navigation in a + hierarchy of menus, and \c dismiss() is the appropriate method for closing + the whole hierarchy of menus. + + \sa popup(), Popup::close() +*/ +void QQuickMenu::dismiss() +{ + QQuickMenu *menu = this; + while (menu) { + menu->close(); + menu = QQuickMenuPrivate::get(menu)->parentMenu; + } +} + +void QQuickMenu::componentComplete() +{ + Q_D(QQuickMenu); + QQuickPopup::componentComplete(); + d->resizeItems(); +} + +void QQuickMenu::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem) +{ + Q_D(QQuickMenu); + QQuickPopup::contentItemChange(newItem, oldItem); + + if (oldItem) { + QQuickItemPrivate::get(oldItem)->removeItemChangeListener(d, QQuickItemPrivate::Children); + QQuickItemPrivate::get(oldItem)->removeItemChangeListener(d, QQuickItemPrivate::Geometry); + } + if (newItem) { + QQuickItemPrivate::get(newItem)->addItemChangeListener(d, QQuickItemPrivate::Children); + QQuickItemPrivate::get(newItem)->updateOrAddGeometryChangeListener(d, QQuickGeometryChange::Width); + } + + d->contentItem = newItem; +} + +void QQuickMenu::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) +{ + Q_D(QQuickMenu); + QQuickPopup::itemChange(change, data); + + if (change == QQuickItem::ItemVisibleHasChanged) { + if (!data.boolValue && d->cascade) { + // Ensure that when the menu isn't visible, there's no current item + // the next time it's opened. + d->setCurrentIndex(-1, Qt::OtherFocusReason); + } + } +} + +void QQuickMenu::keyPressEvent(QKeyEvent *event) +{ + Q_D(QQuickMenu); + QQuickPopup::keyPressEvent(event); + + // QTBUG-17051 + // Work around the fact that ListView has no way of distinguishing between + // mouse and keyboard interaction, thanks to the "interactive" bool in Flickable. + // What we actually want is to have a way to always allow keyboard interaction but + // only allow flicking with the mouse when there are too many menu items to be + // shown at once. + switch (event->key()) { + case Qt::Key_Up: + if (!d->activatePreviousItem()) + d->propagateKeyEvent(event); + break; + + case Qt::Key_Down: + d->activateNextItem(); + break; + + case Qt::Key_Left: + case Qt::Key_Right: + event->ignore(); + if (d->popupItem->isMirrored() == (event->key() == Qt::Key_Right)) { + if (d->parentMenu && d->currentItem) { + if (!d->cascade) + d->parentMenu->open(); + close(); + event->accept(); + } + } else { + if (QQuickMenu *subMenu = d->currentSubMenu()) { + auto subMenuPrivate = QQuickMenuPrivate::get(subMenu); + subMenu->popup(subMenuPrivate->firstEnabledMenuItem()); + event->accept(); + } + } + if (!event->isAccepted()) + d->propagateKeyEvent(event); + break; + +#if QT_CONFIG(shortcut) + case Qt::Key_Alt: + // If &mnemonic shortcut is enabled, go back to (possibly) the parent + // menu bar so the shortcut key will be processed by the menu bar. + if (!QKeySequence::mnemonic(QStringLiteral("&A")).isEmpty()) + close(); + break; +#endif + + default: + break; + } +} + +void QQuickMenu::timerEvent(QTimerEvent *event) +{ + Q_D(QQuickMenu); + if (event->timerId() == d->hoverTimer) { + if (QQuickMenu *subMenu = d->currentSubMenu()) + subMenu->open(); + d->stopHoverTimer(); + return; + } + QQuickPopup::timerEvent(event); +} + +QFont QQuickMenu::defaultFont() const +{ + return QQuickTheme::font(QQuickTheme::Menu); +} + +#if QT_CONFIG(accessibility) +QAccessible::Role QQuickMenu::accessibleRole() const +{ + return QAccessible::PopupMenu; +} +#endif + +QT_END_NAMESPACE + +#include "moc_qquickmenu_p.cpp" |