diff options
author | J-P Nurmi <jpnurmi@qt.io> | 2017-06-01 23:36:57 +0200 |
---|---|---|
committer | J-P Nurmi <jpnurmi@qt.io> | 2017-06-13 09:34:39 +0000 |
commit | 92fcd0a3fb2ee0d1ba242607c1aa5a29864d3650 (patch) | |
tree | e55ace5afc75cfaa17f2571fda8cfae2d8f4e9f8 /src/quicktemplates2/qquickmenu.cpp | |
parent | 23a80af1b5d31621bf1d342706c9541dd5273b86 (diff) |
Add support for cascading sub-menus
This commit adds initial support for cascading sub-menus by allowing
one to nest declarative Menu declarations. A follow-up commit adds
support for adding, inserting, and removing menus programmatically.
[ChangeLog][Controls][Menu] Added support for cascading sub-menus.
Task-number: QTBUG-60351
Change-Id: I0eee4f74d92a97c09333fcc4348b019782448535
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
Diffstat (limited to 'src/quicktemplates2/qquickmenu.cpp')
-rw-r--r-- | src/quicktemplates2/qquickmenu.cpp | 273 |
1 files changed, 245 insertions, 28 deletions
diff --git a/src/quicktemplates2/qquickmenu.cpp b/src/quicktemplates2/qquickmenu.cpp index fa6b88ef..64f293d8 100644 --- a/src/quicktemplates2/qquickmenu.cpp +++ b/src/quicktemplates2/qquickmenu.cpp @@ -131,17 +131,30 @@ QT_BEGIN_NAMESPACE } \endcode - Since QtQuick.Controls 2.3 (Qt 5.10), it is also possible to declare - Action objects inside Menu: + 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 @@ -154,8 +167,24 @@ QT_BEGIN_NAMESPACE \sa {Customizing Menu}, {Menu Controls}, {Popup Controls} */ +static const QQuickPopup::ClosePolicy defaultMenuClosePolicy = + QQuickPopup::CloseOnEscape | QQuickPopup::CloseOnPressOutside | QQuickPopup::CloseOnReleaseOutside; +static const QQuickPopup::ClosePolicy cascadingSubMenuClosePolicy = + QQuickPopup::CloseOnEscape | QQuickPopup::CloseOnPressOutsideParent | QQuickPopup::CloseOnReleaseOutsideParent; + +static bool shouldCascade() +{ +#if QT_CONFIG(cursor) + return QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::MultipleWindows); +#else + return false; +#endif +} + QQuickMenuPrivate::QQuickMenuPrivate() - : contentItem(nullptr), + : cascade(shouldCascade()), + overlap(0), + contentItem(nullptr), contentModel(nullptr), delegate(nullptr) { @@ -184,7 +213,7 @@ void QQuickMenuPrivate::insertItem(int index, QQuickItem *item) Q_Q(QQuickMenu); QQuickMenuItemPrivate::get(menuItem)->setMenu(q); QObjectPrivate::connect(menuItem, &QQuickMenuItem::pressed, this, &QQuickMenuPrivate::onItemPressed); - QObject::connect(menuItem, &QQuickMenuItem::triggered, q, &QQuickPopup::close); + QObjectPrivate::connect(menuItem, &QQuickMenuItem::triggered, this, &QQuickMenuPrivate::onItemTriggered); QObjectPrivate::connect(menuItem, &QQuickItem::activeFocusChanged, this, &QQuickMenuPrivate::onItemActiveFocusChanged); QObjectPrivate::connect(menuItem, &QQuickControl::hoveredChanged, this, &QQuickMenuPrivate::onItemHovered); } @@ -205,16 +234,15 @@ void QQuickMenuPrivate::removeItem(int index, QQuickItem *item) QQuickMenuItem *menuItem = qobject_cast<QQuickMenuItem *>(item); if (menuItem) { - Q_Q(QQuickMenu); QQuickMenuItemPrivate::get(menuItem)->setMenu(nullptr); QObjectPrivate::disconnect(menuItem, &QQuickMenuItem::pressed, this, &QQuickMenuPrivate::onItemPressed); - QObject::disconnect(menuItem, &QQuickMenuItem::triggered, q, &QQuickPopup::close); + 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::createItem(QQuickAction *action) +QQuickItem *QQuickMenuPrivate::beginCreateItem() { Q_Q(QQuickMenu); if (!delegate) @@ -227,15 +255,37 @@ QQuickItem *QQuickMenuPrivate::createItem(QQuickAction *action) context->setContextObject(q); QObject *object = delegate->beginCreate(context); - if (QQuickItem *item = qobject_cast<QQuickItem *>(object)) { - if (QQuickAbstractButton *button = qobject_cast<QQuickAbstractButton*>(object)) - button->setAction(action); - delegate->completeCreate(); - return item; - } + QQuickItem *item = qobject_cast<QQuickItem *>(object); + if (!item) + delete object; + + return item; +} + +void QQuickMenuPrivate::completeCreateItem() +{ + if (!delegate) + return; - delete object; - return nullptr; + 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) @@ -299,6 +349,12 @@ void QQuickMenuPrivate::itemGeometryChanged(QQuickItem *, QQuickGeometryChange, resizeItems(); } +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::onItemPressed() { Q_Q(QQuickMenu); @@ -314,9 +370,40 @@ void QQuickMenuPrivate::onItemHovered() if (!button || !button->isHovered() || QQuickAbstractButtonPrivate::get(button)->touchId != -1) return; + QQuickMenuItem *oldCurrentItem = currentItem; + int index = contentModel->indexOf(button, nullptr); - if (index != -1) + if (index != -1) { setCurrentIndex(index); + if (oldCurrentItem != currentItem) { + if (oldCurrentItem) + closeSubMenu(oldCurrentItem->subMenu()); + if (currentItem) { + QQuickMenu *subMenu = currentItem->menu(); + if (subMenu && subMenu->cascade()) + openSubMenu(currentItem, false); + } + } + } +} + +void QQuickMenuPrivate::onItemTriggered() +{ + Q_Q(QQuickMenu); + QQuickMenuItem *item = qobject_cast<QQuickMenuItem *>(q->sender()); + if (!item) + return; + + if (item->subMenu()) { + openSubMenu(item, true); + } else { + // close the whole chain of menus + q->close(); + while (parentMenu) { + parentMenu->close(); + parentMenu = QQuickMenuPrivate::get(parentMenu)->parentMenu; + } + } } void QQuickMenuPrivate::onItemActiveFocusChanged() @@ -330,6 +417,67 @@ void QQuickMenuPrivate::onItemActiveFocusChanged() setCurrentIndex(indexOfItem); } +void QQuickMenuPrivate::openSubMenu(QQuickMenuItem *item, bool activate) +{ + Q_Q(QQuickMenu); + QQuickMenu *subMenu = item ? item->subMenu() : nullptr; + if (!subMenu) + return; + + if (cascade) { + subMenu->setParentItem(item); + subMenu->setClosePolicy(cascadingSubMenuClosePolicy); + if (popupItem->isMirrored()) { + subMenu->setTransformOrigin(QQuickPopup::TopRight); + subMenu->setPosition(QPointF(-subMenu->width() - q->leftPadding() + subMenu->overlap(), -subMenu->topPadding())); + } else { + subMenu->setTransformOrigin(QQuickPopup::TopLeft); + subMenu->setPosition(QPointF(item->width() + q->rightPadding() - subMenu->overlap(), -subMenu->topPadding())); + } + } else { + subMenu->setParentItem(parentItem); + subMenu->setClosePolicy(defaultMenuClosePolicy); + subMenu->setTransformOrigin(QQuickPopup::Center); + subMenu->setPosition(QPointF(q->x() + (q->width() - subMenu->width()) / 2, + q->y() + (q->height() - subMenu->height()) / 2)); + } + + QQuickMenuPrivate *p = QQuickMenuPrivate::get(subMenu); + p->parentMenu = q; + if (activate) + p->setCurrentIndex(0); + subMenu->setCascade(cascade); + subMenu->open(); + + // transfer focus to the sub-menu + if (focus) + subMenu->popupItem()->setFocus(true); + + if (!subMenu->cascade()) + q->close(); +} + +void QQuickMenuPrivate::closeSubMenu(QQuickMenu *subMenu) +{ + if (!subMenu || !subMenu->isVisible()) + return; + + // transfer focus back to the parent menu + QQuickMenu *parentMenu = QQuickMenuPrivate::get(subMenu)->parentMenu; + if (parentMenu && parentMenu->hasFocus()) { + parentMenu->popupItem()->setFocus(true); + if (!subMenu->cascade()) + parentMenu->open(); + } + + // close the whole chain of sub-menus + while (subMenu) { + QPointer<QQuickMenuItem> currentSubMenuItem = QQuickMenuPrivate::get(subMenu)->currentItem; + subMenu->close(); + subMenu = currentSubMenuItem ? currentSubMenuItem->subMenu() : nullptr; + } +} + int QQuickMenuPrivate::currentIndex() const { QVariant index = contentItem->property("currentIndex"); @@ -355,13 +503,15 @@ void QQuickMenuPrivate::setCurrentIndex(int index) void QQuickMenuPrivate::contentData_append(QQmlListProperty<QObject> *prop, QObject *obj) { - QQuickMenuPrivate *p = static_cast<QQuickMenuPrivate *>(prop->data); - QQuickMenu *q = static_cast<QQuickMenu *>(prop->object); + 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) { @@ -378,27 +528,27 @@ void QQuickMenuPrivate::contentData_append(QQmlListProperty<QObject> *prop, QObj int QQuickMenuPrivate::contentData_count(QQmlListProperty<QObject> *prop) { - QQuickMenuPrivate *p = static_cast<QQuickMenuPrivate *>(prop->data); - return p->contentData.count(); + QQuickMenu *q = static_cast<QQuickMenu *>(prop->object); + return QQuickMenuPrivate::get(q)->contentData.count(); } QObject *QQuickMenuPrivate::contentData_at(QQmlListProperty<QObject> *prop, int index) { - QQuickMenuPrivate *p = static_cast<QQuickMenuPrivate *>(prop->data); - return p->contentData.value(index); + QQuickMenu *q = static_cast<QQuickMenu *>(prop->object); + return QQuickMenuPrivate::get(q)->contentData.value(index); } void QQuickMenuPrivate::contentData_clear(QQmlListProperty<QObject> *prop) { - QQuickMenuPrivate *p = static_cast<QQuickMenuPrivate *>(prop->data); - p->contentData.clear(); + QQuickMenu *q = static_cast<QQuickMenu *>(prop->object); + QQuickMenuPrivate::get(q)->contentData.clear(); } QQuickMenu::QQuickMenu(QObject *parent) : QQuickPopup(*(new QQuickMenuPrivate), parent) { setFocus(true); - setClosePolicy(CloseOnEscape | CloseOnPressOutside | CloseOnReleaseOutside); + setClosePolicy(defaultMenuClosePolicy); } /*! @@ -568,8 +718,7 @@ QVariant QQuickMenu::contentModel() const */ QQmlListProperty<QObject> QQuickMenu::contentData() { - Q_D(QQuickMenu); - return QQmlListProperty<QObject>(this, d, + return QQmlListProperty<QObject>(this, nullptr, QQuickMenuPrivate::contentData_append, QQuickMenuPrivate::contentData_count, QQuickMenuPrivate::contentData_at, @@ -602,6 +751,64 @@ void QQuickMenu::setTitle(QString &title) /*! \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; + emit cascadeChanged(); +} + +/*! + \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 cascadeChanged(); +} + +/*! + \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 @@ -758,7 +965,7 @@ void QQuickMenu::itemChange(QQuickItem::ItemChange change, const QQuickItem::Ite QQuickPopup::itemChange(change, data); if (change == QQuickItem::ItemVisibleHasChanged) { - if (!data.boolValue) { + if (!data.boolValue && d->cascade) { // Ensure that when the menu isn't visible, there's no current item // the next time it's opened. QQuickItem *focusItem = QQuickItemPrivate::get(d->contentItem)->subFocusItem; @@ -796,6 +1003,16 @@ void QQuickMenu::keyReleaseEvent(QKeyEvent *event) QMetaObject::invokeMethod(d->contentItem, "incrementCurrentIndex"); break; + case Qt::Key_Left: + case Qt::Key_Right: + if (d->popupItem->isMirrored() == (event->key() == Qt::Key_Right)) { + if (d->parentMenu && d->currentItem) + d->closeSubMenu(this); + } else { + d->openSubMenu(d->currentItem, true); + } + return; + default: break; } |