diff options
26 files changed, 910 insertions, 38 deletions
diff --git a/src/imports/controls/Menu.qml b/src/imports/controls/Menu.qml index 31c9230a..1fbf9a92 100644 --- a/src/imports/controls/Menu.qml +++ b/src/imports/controls/Menu.qml @@ -48,6 +48,7 @@ T.Menu { contentItem ? contentItem.implicitHeight : 0) + topPadding + bottomPadding margins: 0 + overlap: 1 delegate: MenuItem { } diff --git a/src/imports/controls/MenuItem.qml b/src/imports/controls/MenuItem.qml index 97c3f965..e0532719 100644 --- a/src/imports/controls/MenuItem.qml +++ b/src/imports/controls/MenuItem.qml @@ -57,8 +57,10 @@ T.MenuItem { icon.color: enabled ? Default.textDarkColor : Default.textDisabledColor contentItem: IconLabel { - leftPadding: control.checkable && !control.mirrored ? control.indicator.width + control.spacing : 0 - rightPadding: control.checkable && control.mirrored ? control.indicator.width + control.spacing : 0 + readonly property real arrowPadding: control.subMenu && control.arrow ? control.arrow.width + control.spacing : 0 + readonly property real indicatorPadding: control.checkable && control.indicator ? control.indicator.width + control.spacing : 0 + leftPadding: !control.mirrored ? indicatorPadding : arrowPadding + rightPadding: control.mirrored ? indicatorPadding : arrowPadding spacing: control.spacing mirrored: control.mirrored @@ -79,6 +81,15 @@ T.MenuItem { source: control.checkable ? "qrc:/qt-project.org/imports/QtQuick/Controls.2/images/check.png" : "" } + arrow: Image { + x: control.mirrored ? control.leftPadding : control.width - width - control.rightPadding + y: control.topPadding + (control.availableHeight - height) / 2 + + visible: control.subMenu + mirror: control.mirrored + source: control.subMenu ? "qrc:/qt-project.org/imports/QtQuick/Controls.2/images/arrow-indicator.png" : "" + } + background: Item { implicitWidth: 200 implicitHeight: 40 diff --git a/src/imports/controls/fusion/Menu.qml b/src/imports/controls/fusion/Menu.qml index 3fd4caa2..d7fceb92 100644 --- a/src/imports/controls/fusion/Menu.qml +++ b/src/imports/controls/fusion/Menu.qml @@ -51,6 +51,7 @@ T.Menu { margins: 0 padding: 1 + overlap: 2 delegate: MenuItem { } diff --git a/src/imports/controls/fusion/MenuItem.qml b/src/imports/controls/fusion/MenuItem.qml index c0127ed7..ca828c40 100644 --- a/src/imports/controls/fusion/MenuItem.qml +++ b/src/imports/controls/fusion/MenuItem.qml @@ -58,8 +58,10 @@ T.MenuItem { icon.height: 16 contentItem: IconLabel { - leftPadding: !control.mirrored ? control.indicator.width + control.spacing : 0 - rightPadding: control.mirrored ? control.indicator.width + control.spacing : 0 + readonly property real arrowPadding: control.subMenu && control.arrow ? control.arrow.width + control.spacing : 0 + readonly property real indicatorPadding: control.checkable && control.indicator ? control.indicator.width + control.spacing : 0 + leftPadding: !control.mirrored ? indicatorPadding : arrowPadding + rightPadding: control.mirrored ? indicatorPadding : arrowPadding spacing: control.spacing mirrored: control.mirrored @@ -72,6 +74,18 @@ T.MenuItem { color: control.down || control.highlighted ? Fusion.highlightedText(control.palette) : control.palette.text } + arrow: ColorImage { + x: control.mirrored ? control.padding : control.width - width - control.padding + y: control.topPadding + (control.availableHeight - height) / 2 + width: 20 + + visible: control.subMenu + rotation: control.mirrored ? 90 : -90 + color: control.down || control.hovered || control.highlighted ? Fusion.highlightedText(control.palette) : control.palette.text + source: "qrc:/qt-project.org/imports/QtQuick/Controls.2/Fusion/images/arrow.png" + fillMode: Image.Pad + } + indicator: CheckIndicator { x: control.mirrored ? control.width - width - control.rightPadding : control.leftPadding y: control.topPadding + (control.availableHeight - height) / 2 diff --git a/src/imports/controls/images/arrow-indicator.png b/src/imports/controls/images/arrow-indicator.png Binary files differnew file mode 100644 index 00000000..50f230dc --- /dev/null +++ b/src/imports/controls/images/arrow-indicator.png diff --git a/src/imports/controls/images/arrow-indicator@2x.png b/src/imports/controls/images/arrow-indicator@2x.png Binary files differnew file mode 100644 index 00000000..457cdde0 --- /dev/null +++ b/src/imports/controls/images/arrow-indicator@2x.png diff --git a/src/imports/controls/images/arrow-indicator@3x.png b/src/imports/controls/images/arrow-indicator@3x.png Binary files differnew file mode 100644 index 00000000..8d624154 --- /dev/null +++ b/src/imports/controls/images/arrow-indicator@3x.png diff --git a/src/imports/controls/images/arrow-indicator@4x.png b/src/imports/controls/images/arrow-indicator@4x.png Binary files differnew file mode 100644 index 00000000..7d2c49e2 --- /dev/null +++ b/src/imports/controls/images/arrow-indicator@4x.png diff --git a/src/imports/controls/material/MenuItem.qml b/src/imports/controls/material/MenuItem.qml index a5bf8a54..743f1e12 100644 --- a/src/imports/controls/material/MenuItem.qml +++ b/src/imports/controls/material/MenuItem.qml @@ -67,9 +67,21 @@ T.MenuItem { control: control } + arrow: ColorImage { + x: control.mirrored ? control.padding : control.width - width - control.padding + y: control.topPadding + (control.availableHeight - height) / 2 + + visible: control.subMenu + mirror: control.mirrored + color: control.enabled ? control.Material.foreground : control.Material.hintTextColor + source: "qrc:/qt-project.org/imports/QtQuick/Controls.2/Material/images/arrow-indicator.png" + } + contentItem: IconLabel { - leftPadding: control.checkable && !control.mirrored ? control.indicator.width + control.spacing : 0 - rightPadding: control.checkable && control.mirrored ? control.indicator.width + control.spacing : 0 + readonly property real arrowPadding: control.subMenu && control.arrow ? control.arrow.width + control.spacing : 0 + readonly property real indicatorPadding: control.checkable && control.indicator ? control.indicator.width + control.spacing : 0 + leftPadding: !control.mirrored ? indicatorPadding : arrowPadding + rightPadding: control.mirrored ? indicatorPadding : arrowPadding spacing: control.spacing mirrored: control.mirrored diff --git a/src/imports/controls/material/images/arrow-indicator.png b/src/imports/controls/material/images/arrow-indicator.png Binary files differnew file mode 100644 index 00000000..4a942849 --- /dev/null +++ b/src/imports/controls/material/images/arrow-indicator.png diff --git a/src/imports/controls/material/images/arrow-indicator.svg b/src/imports/controls/material/images/arrow-indicator.svg new file mode 100644 index 00000000..1e7217c8 --- /dev/null +++ b/src/imports/controls/material/images/arrow-indicator.svg @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="5" + height="10" + viewBox="0 0 5 10" + version="1.1" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="arrow-indicator.svg"> + <metadata + id="metadata10"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs8" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="2560" + inkscape:window-height="1571" + id="namedview6" + showgrid="false" + inkscape:zoom="27.812867" + inkscape:cx="13.137558" + inkscape:cy="12.83583" + inkscape:window-x="0" + inkscape:window-y="55" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /> + <path + d="M 0,10 5,5 0,0 Z" + id="path4" + inkscape:connector-curvature="0" + style="fill:#757575" /> +</svg> diff --git a/src/imports/controls/material/images/arrow-indicator@2x.png b/src/imports/controls/material/images/arrow-indicator@2x.png Binary files differnew file mode 100644 index 00000000..b31b56c1 --- /dev/null +++ b/src/imports/controls/material/images/arrow-indicator@2x.png diff --git a/src/imports/controls/material/images/arrow-indicator@3x.png b/src/imports/controls/material/images/arrow-indicator@3x.png Binary files differnew file mode 100644 index 00000000..637e9674 --- /dev/null +++ b/src/imports/controls/material/images/arrow-indicator@3x.png diff --git a/src/imports/controls/material/images/arrow-indicator@4x.png b/src/imports/controls/material/images/arrow-indicator@4x.png Binary files differnew file mode 100644 index 00000000..15b9a902 --- /dev/null +++ b/src/imports/controls/material/images/arrow-indicator@4x.png diff --git a/src/imports/controls/material/qtquickcontrols2materialstyleplugin.qrc b/src/imports/controls/material/qtquickcontrols2materialstyleplugin.qrc index e2cc90fa..6e29aea4 100644 --- a/src/imports/controls/material/qtquickcontrols2materialstyleplugin.qrc +++ b/src/imports/controls/material/qtquickcontrols2materialstyleplugin.qrc @@ -1,5 +1,9 @@ <RCC> <qresource prefix="/qt-project.org/imports/QtQuick/Controls.2/Material"> + <file>images/arrow-indicator.png</file> + <file>images/arrow-indicator@2x.png</file> + <file>images/arrow-indicator@3x.png</file> + <file>images/arrow-indicator@4x.png</file> <file>images/check.png</file> <file>images/check@2x.png</file> <file>images/check@3x.png</file> diff --git a/src/imports/controls/qtquickcontrols2plugin.qrc b/src/imports/controls/qtquickcontrols2plugin.qrc index 0948a529..c2dd9d39 100644 --- a/src/imports/controls/qtquickcontrols2plugin.qrc +++ b/src/imports/controls/qtquickcontrols2plugin.qrc @@ -1,5 +1,9 @@ <RCC> <qresource prefix="/qt-project.org/imports/QtQuick/Controls.2"> + <file>images/arrow-indicator.png</file> + <file>images/arrow-indicator@2x.png</file> + <file>images/arrow-indicator@3x.png</file> + <file>images/arrow-indicator@4x.png</file> <file>images/check.png</file> <file>images/check@2x.png</file> <file>images/check@3x.png</file> diff --git a/src/imports/controls/universal/Menu.qml b/src/imports/controls/universal/Menu.qml index d7a27563..b2e71106 100644 --- a/src/imports/controls/universal/Menu.qml +++ b/src/imports/controls/universal/Menu.qml @@ -48,6 +48,7 @@ T.Menu { contentItem ? contentItem.implicitHeight : 0) + topPadding + bottomPadding margins: 0 + overlap: 1 delegate: MenuItem { } diff --git a/src/imports/controls/universal/MenuItem.qml b/src/imports/controls/universal/MenuItem.qml index 7924e025..56f2eafa 100644 --- a/src/imports/controls/universal/MenuItem.qml +++ b/src/imports/controls/universal/MenuItem.qml @@ -60,8 +60,10 @@ T.MenuItem { icon.color: !enabled ? Universal.baseLowColor : Universal.baseHighColor contentItem: IconLabel { - leftPadding: control.checkable && !control.mirrored ? control.indicator.width + control.spacing : 0 - rightPadding: control.checkable && control.mirrored ? control.indicator.width + control.spacing : 0 + readonly property real arrowPadding: control.subMenu && control.arrow ? control.arrow.width + control.spacing : 0 + readonly property real indicatorPadding: control.checkable && control.indicator ? control.indicator.width + control.spacing : 0 + leftPadding: !control.mirrored ? indicatorPadding : arrowPadding + rightPadding: control.mirrored ? indicatorPadding : arrowPadding spacing: control.spacing mirrored: control.mirrored @@ -74,6 +76,16 @@ T.MenuItem { color: !control.enabled ? control.Universal.baseLowColor : control.Universal.baseHighColor } + arrow: ColorImage { + x: control.mirrored ? control.leftPadding : control.width - width - control.rightPadding + y: control.topPadding + (control.availableHeight - height) / 2 + + visible: control.subMenu + mirror: control.mirrored + color: !enabled ? control.Universal.baseLowColor : control.Universal.baseHighColor + source: "qrc:/qt-project.org/imports/QtQuick/Controls.2/Universal/images/rightarrow.png" + } + indicator: ColorImage { x: control.text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2 y: control.topPadding + (control.availableHeight - height) / 2 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; } diff --git a/src/quicktemplates2/qquickmenu_p.h b/src/quicktemplates2/qquickmenu_p.h index 04186f1a..f7f56d45 100644 --- a/src/quicktemplates2/qquickmenu_p.h +++ b/src/quicktemplates2/qquickmenu_p.h @@ -65,6 +65,8 @@ class Q_QUICKTEMPLATES2_PRIVATE_EXPORT QQuickMenu : public QQuickPopup Q_PROPERTY(QVariant contentModel READ contentModel CONSTANT FINAL) Q_PROPERTY(QQmlListProperty<QObject> contentData READ contentData FINAL) Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged FINAL) + Q_PROPERTY(bool cascade READ cascade WRITE setCascade NOTIFY cascadeChanged FINAL REVISION 3) + Q_PROPERTY(qreal overlap READ overlap WRITE setOverlap NOTIFY overlapChanged FINAL REVISION 3) Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged FINAL REVISION 3) Q_CLASSINFO("DefaultProperty", "contentData") @@ -85,6 +87,12 @@ public: QString title() const; void setTitle(QString &title); + bool cascade() const; + void setCascade(bool cascade); + + qreal overlap() const; + void setOverlap(qreal overlap); + QQmlComponent *delegate() const; void setDelegate(QQmlComponent *delegate); @@ -98,6 +106,8 @@ protected: Q_SIGNALS: void titleChanged(const QString &title); + Q_REVISION(3) void cascadeChanged(); + Q_REVISION(3) void overlapChanged(); Q_REVISION(3) void delegateChanged(); protected: diff --git a/src/quicktemplates2/qquickmenu_p_p.h b/src/quicktemplates2/qquickmenu_p_p.h index b9e0512b..9fd10779 100644 --- a/src/quicktemplates2/qquickmenu_p_p.h +++ b/src/quicktemplates2/qquickmenu_p_p.h @@ -51,11 +51,11 @@ #include <QtCore/qvector.h> #include <QtCore/qpointer.h> +#include <QtQuickTemplates2/private/qquickmenu_p.h> #include <QtQuickTemplates2/private/qquickpopup_p_p.h> QT_BEGIN_NAMESPACE -class QQuickMenu; class QQuickAction; class QQmlComponent; class QQmlObjectModel; @@ -68,10 +68,20 @@ class Q_QUICKTEMPLATES2_PRIVATE_EXPORT QQuickMenuPrivate : public QQuickPopupPri public: QQuickMenuPrivate(); + static QQuickMenuPrivate *get(QQuickMenu *menu) + { + return menu->d_func(); + } + QQuickItem *itemAt(int index) const; void insertItem(int index, QQuickItem *item); void moveItem(int from, int to); void removeItem(int index, QQuickItem *item); + + QQuickItem *beginCreateItem(); + void completeCreateItem(); + + QQuickItem *createItem(QQuickMenu *menu); QQuickItem *createItem(QQuickAction *action); void resizeItem(QQuickItem *item); @@ -83,10 +93,16 @@ public: void itemDestroyed(QQuickItem *item) override; void itemGeometryChanged(QQuickItem *, QQuickGeometryChange change, const QRectF &diff) override; + bool blockInput(QQuickItem *item, const QPointF &point) const override; + void onItemPressed(); void onItemHovered(); + void onItemTriggered(); void onItemActiveFocusChanged(); + void openSubMenu(QQuickMenuItem *item, bool activate); + void closeSubMenu(QQuickMenu *subMenu); + int currentIndex() const; void setCurrentIndex(int index); @@ -95,6 +111,9 @@ public: static QObject *contentData_at(QQmlListProperty<QObject> *prop, int index); static void contentData_clear(QQmlListProperty<QObject> *prop); + bool cascade; + qreal overlap; + QPointer<QQuickMenu> parentMenu; QPointer<QQuickMenuItem> currentItem; QQuickItem *contentItem; // TODO: cleanup QVector<QObject *> contentData; diff --git a/src/quicktemplates2/qquickmenuitem.cpp b/src/quicktemplates2/qquickmenuitem.cpp index 3ef03caf..69a2b73b 100644 --- a/src/quicktemplates2/qquickmenuitem.cpp +++ b/src/quicktemplates2/qquickmenuitem.cpp @@ -36,6 +36,7 @@ #include "qquickmenuitem_p.h" #include "qquickmenuitem_p_p.h" +#include "qquickmenu_p.h" #include <QtGui/qpa/qplatformtheme.h> #include <QtQuick/private/qquickevents_p_p.h> @@ -89,7 +90,9 @@ QT_BEGIN_NAMESPACE QQuickMenuItemPrivate::QQuickMenuItemPrivate() : highlighted(false), - menu(nullptr) + arrow(nullptr), + menu(nullptr), + subMenu(nullptr) { } @@ -103,6 +106,24 @@ void QQuickMenuItemPrivate::setMenu(QQuickMenu *newMenu) emit q->menuChanged(); } +void QQuickMenuItemPrivate::setSubMenu(QQuickMenu *newSubMenu) +{ + Q_Q(QQuickMenuItem); + if (subMenu == newSubMenu) + return; + + if (subMenu) + QObject::disconnect(subMenu, &QQuickMenu::titleChanged, q, &QQuickAbstractButton::setText); + + if (newSubMenu) { + QObject::connect(newSubMenu, &QQuickMenu::titleChanged, q, &QQuickAbstractButton::setText); + q->setText(newSubMenu->title()); + } + + subMenu = newSubMenu; + emit q->subMenuChanged(); +} + /*! \qmlsignal void QtQuick.Controls::MenuItem::triggered() @@ -142,6 +163,33 @@ void QQuickMenuItem::setHighlighted(bool highlighted) /*! \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlproperty Item QtQuick.Controls::MenuItem::arrow + + This property holds the sub-menu arrow item. + + \sa {Customizing MenuItem} +*/ +QQuickItem *QQuickMenuItem::arrow() const +{ + Q_D(const QQuickMenuItem); + return d->arrow; +} + +void QQuickMenuItem::setArrow(QQuickItem *arrow) +{ + Q_D(QQuickMenuItem); + if (d->arrow == arrow) + return; + + QQuickControlPrivate::destroyDelegate(d->arrow, this); + d->arrow = arrow; + if (arrow && !arrow->parentItem()) + arrow->setParentItem(this); + emit arrowChanged(); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) \qmlproperty Menu QtQuick.Controls::MenuItem::menu \readonly @@ -154,6 +202,20 @@ QQuickMenu *QQuickMenuItem::menu() const return d->menu; } +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlproperty Menu QtQuick.Controls::MenuItem::subMenu + \readonly + + This property holds the sub-menu that this item presents in + the parent menu, or \c null if this item is not a sub-menu item. +*/ +QQuickMenu *QQuickMenuItem::subMenu() const +{ + Q_D(const QQuickMenuItem); + return d->subMenu; +} + QFont QQuickMenuItem::defaultFont() const { return QQuickControlPrivate::themeFont(QPlatformTheme::MenuItemFont); diff --git a/src/quicktemplates2/qquickmenuitem_p.h b/src/quicktemplates2/qquickmenuitem_p.h index 92b42488..7c469b6a 100644 --- a/src/quicktemplates2/qquickmenuitem_p.h +++ b/src/quicktemplates2/qquickmenuitem_p.h @@ -59,7 +59,9 @@ class Q_QUICKTEMPLATES2_PRIVATE_EXPORT QQuickMenuItem : public QQuickAbstractBut { Q_OBJECT Q_PROPERTY(bool highlighted READ isHighlighted WRITE setHighlighted NOTIFY highlightedChanged FINAL) + Q_PROPERTY(QQuickItem *arrow READ arrow WRITE setArrow NOTIFY arrowChanged FINAL REVISION 3) Q_PROPERTY(QQuickMenu *menu READ menu NOTIFY menuChanged FINAL REVISION 3) + Q_PROPERTY(QQuickMenu *subMenu READ subMenu NOTIFY subMenuChanged FINAL REVISION 3) public: explicit QQuickMenuItem(QQuickItem *parent = nullptr); @@ -67,12 +69,18 @@ public: bool isHighlighted() const; void setHighlighted(bool highlighted); + QQuickItem *arrow() const; + void setArrow(QQuickItem *arrow); + QQuickMenu *menu() const; + QQuickMenu *subMenu() const; Q_SIGNALS: void triggered(); void highlightedChanged(); + Q_REVISION(3) void arrowChanged(); Q_REVISION(3) void menuChanged(); + Q_REVISION(3) void subMenuChanged(); protected: QFont defaultFont() const override; diff --git a/src/quicktemplates2/qquickmenuitem_p_p.h b/src/quicktemplates2/qquickmenuitem_p_p.h index f1cded91..d8b86d6e 100644 --- a/src/quicktemplates2/qquickmenuitem_p_p.h +++ b/src/quicktemplates2/qquickmenuitem_p_p.h @@ -68,9 +68,12 @@ public: } void setMenu(QQuickMenu *menu); + void setSubMenu(QQuickMenu *subMenu); bool highlighted; + QQuickItem *arrow; QQuickMenu *menu; + QQuickMenu *subMenu; }; QT_END_NAMESPACE diff --git a/tests/auto/menu/data/subMenus.qml b/tests/auto/menu/data/subMenus.qml new file mode 100644 index 00000000..314e7fed --- /dev/null +++ b/tests/auto/menu/data/subMenus.qml @@ -0,0 +1,96 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.9 +import QtQuick.Controls 2.3 + +ApplicationWindow { + width: 400 + height: 400 + + property alias mainMenu: mainMenu + property alias subMenu1: subMenu1 + property alias subMenu2: subMenu2 + property alias subSubMenu1: subSubMenu1 + + Menu { + id: mainMenu + width: 100 + MenuItem { id: mainMenuItem1; text: "Main 1" } + + Menu { + id: subMenu1 + width: 100 + title: "Sub Menu 1" + MenuItem { id: subMenuItem1; text: "Sub 1" } + MenuItem { id: subMenuItem2; text: "Sub 2" } + + Menu { + id: subSubMenu1 + width: 100 + title: "Sub Sub Menu 1" + MenuItem { id: subSubMenuItem1; text: "Sub Sub 1" } + MenuItem { id: subSubMenuItem2; text: "Sub Sub 2" } + } + } + + MenuItem { id: mainMenuItem2; text: "Main 2" } + + Menu { + id: subMenu2 + width: 100 + title: "Sub Menu 2" + MenuItem { id: subMenuItem3; text: "Sub 3" } + MenuItem { id: subMenuItem4; text: "Sub 4" } + } + + MenuItem { id: mainMenuItem3; text: "Main 3" } + } +} diff --git a/tests/auto/menu/tst_menu.cpp b/tests/auto/menu/tst_menu.cpp index 7e892025..f39c391c 100644 --- a/tests/auto/menu/tst_menu.cpp +++ b/tests/auto/menu/tst_menu.cpp @@ -73,6 +73,12 @@ private slots: void popup(); void actions(); void removeTakeItem(); + void subMenuMouse_data(); + void subMenuMouse(); + void subMenuKeyboard_data(); + void subMenuKeyboard(); + void subMenuPosition_data(); + void subMenuPosition(); }; void tst_menu::defaults() @@ -574,6 +580,341 @@ void tst_menu::removeTakeItem() QVERIFY(!menuItem3.isNull()); } +void tst_menu::subMenuMouse_data() +{ + QTest::addColumn<bool>("cascade"); + + QTest::newRow("cascading") << true; + QTest::newRow("non-cascading") << false; +} + +void tst_menu::subMenuMouse() +{ + QFETCH(bool, cascade); + + QQuickApplicationHelper helper(this, QLatin1String("subMenus.qml")); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + QVERIFY(QTest::qWaitForWindowActive(window)); + moveMouseAway(window); + + QQuickMenu *mainMenu = window->property("mainMenu").value<QQuickMenu *>(); + QVERIFY(mainMenu); + mainMenu->setCascade(cascade); + QCOMPARE(mainMenu->cascade(), cascade); + + QQuickMenu *subMenu1 = window->property("subMenu1").value<QQuickMenu *>(); + QVERIFY(subMenu1); + + QQuickMenu *subMenu2 = window->property("subMenu2").value<QQuickMenu *>(); + QVERIFY(subMenu2); + + QQuickMenu *subSubMenu1 = window->property("subSubMenu1").value<QQuickMenu *>(); + QVERIFY(subSubMenu1); + + mainMenu->open(); + QVERIFY(mainMenu->isVisible()); + QVERIFY(!subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + + // open the sub-menu with mouse click + QQuickMenuItem *subMenu1Item = qobject_cast<QQuickMenuItem *>(mainMenu->itemAt(1)); + QVERIFY(subMenu1Item); + QCOMPARE(subMenu1Item->subMenu(), subMenu1); + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, subMenu1Item->mapToScene(QPoint(1, 1)).toPoint()); + QCOMPARE(mainMenu->isVisible(), cascade); + QVERIFY(subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + + // open the cascading sub-sub-menu with mouse hover + QQuickMenuItem *subSubMenu1Item = qobject_cast<QQuickMenuItem *>(subMenu1->itemAt(2)); + QVERIFY(subSubMenu1Item); + QCOMPARE(subSubMenu1Item->subMenu(), subSubMenu1); + QTest::mouseMove(window, subSubMenu1Item->mapToScene(QPoint(1, 1)).toPoint()); + QCOMPARE(mainMenu->isVisible(), cascade); + QVERIFY(subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QCOMPARE(subSubMenu1->isVisible(), cascade); + + // close the sub-sub-menu with mouse hover over another parent menu item + QQuickMenuItem *subMenuItem1 = qobject_cast<QQuickMenuItem *>(subMenu1->itemAt(0)); + QVERIFY(subMenuItem1); + QVERIFY(!subMenuItem1->subMenu()); + QTest::mouseMove(window, subMenuItem1->mapToScene(QPoint(1, 1)).toPoint()); + QCOMPARE(mainMenu->isVisible(), cascade); + QVERIFY(subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + + // re-open the sub-sub-menu with mouse hover + QTest::mouseMove(window, subSubMenu1Item->mapToScene(QPoint(1, 1)).toPoint()); + QCOMPARE(mainMenu->isVisible(), cascade); + QVERIFY(subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QCOMPARE(subSubMenu1->isVisible(), cascade); + + // close sub-menu and sub-sub-menu with mouse hover in the main menu + QQuickMenuItem *mainMenuItem1 = qobject_cast<QQuickMenuItem *>(mainMenu->itemAt(0)); + QVERIFY(mainMenuItem1); + QTest::mouseMove(window, mainMenuItem1->mapToScene(QPoint(1, 1)).toPoint()); + QCOMPARE(mainMenu->isVisible(), cascade); + QCOMPARE(subMenu1->isVisible(), !cascade); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + + // close all menus by click triggering an item + QQuickMenuItem *subSubMenuItem1 = qobject_cast<QQuickMenuItem *>(subSubMenu1->itemAt(0)); + QVERIFY(subSubMenuItem1); + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, subSubMenuItem1->mapToScene(QPoint(1, 1)).toPoint()); + QVERIFY(!mainMenu->isVisible()); + QVERIFY(!subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); +} + +void tst_menu::subMenuKeyboard_data() +{ + QTest::addColumn<bool>("cascade"); + QTest::addColumn<bool>("mirrored"); + + QTest::newRow("cascading") << true << false; + QTest::newRow("cascading,mirrored") << true << true; + QTest::newRow("non-cascading") << false << false; + QTest::newRow("non-cascading,mirrored") << false << true; +} + +void tst_menu::subMenuKeyboard() +{ + QFETCH(bool, cascade); + QFETCH(bool, mirrored); + + QQuickApplicationHelper helper(this, QLatin1String("subMenus.qml")); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + QVERIFY(QTest::qWaitForWindowActive(window)); + moveMouseAway(window); + + if (mirrored) + window->setLocale(QLocale("ar_EG")); + + QQuickMenu *mainMenu = window->property("mainMenu").value<QQuickMenu *>(); + QVERIFY(mainMenu); + mainMenu->setCascade(cascade); + QCOMPARE(mainMenu->cascade(), cascade); + + QQuickMenu *subMenu1 = window->property("subMenu1").value<QQuickMenu *>(); + QVERIFY(subMenu1); + + QQuickMenu *subMenu2 = window->property("subMenu2").value<QQuickMenu *>(); + QVERIFY(subMenu2); + + QQuickMenu *subSubMenu1 = window->property("subSubMenu1").value<QQuickMenu *>(); + QVERIFY(subSubMenu1); + + mainMenu->open(); + QVERIFY(mainMenu->isVisible()); + QVERIFY(!subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + + // navigate to the sub-menu item and trigger it + QQuickMenuItem *subMenu1Item = qobject_cast<QQuickMenuItem *>(mainMenu->itemAt(1)); + QVERIFY(subMenu1Item); + QVERIFY(!subMenu1Item->isHighlighted()); + QCOMPARE(subMenu1Item->subMenu(), subMenu1); + QTest::keyClick(window, Qt::Key_Down); + QTest::keyClick(window, Qt::Key_Down); + QVERIFY(subMenu1Item->isHighlighted()); + QTest::keyClick(window, Qt::Key_Space); + QCOMPARE(mainMenu->isVisible(), cascade); + QVERIFY(subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + + // navigate to the sub-sub-menu item and open it with the arrow key + QQuickMenuItem *subSubMenu1Item = qobject_cast<QQuickMenuItem *>(subMenu1->itemAt(2)); + QVERIFY(subSubMenu1Item); + QVERIFY(!subSubMenu1Item->isHighlighted()); + QCOMPARE(subSubMenu1Item->subMenu(), subSubMenu1); + QTest::keyClick(window, Qt::Key_Down); + QTest::keyClick(window, Qt::Key_Down); + QTest::keyClick(window, Qt::Key_Down); + QVERIFY(subSubMenu1Item->isHighlighted()); + QCOMPARE(mainMenu->isVisible(), cascade); + QVERIFY(subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + QTest::keyClick(window, mirrored ? Qt::Key_Left : Qt::Key_Right); + QCOMPARE(mainMenu->isVisible(), cascade); + QCOMPARE(subMenu1->isVisible(), cascade); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(subSubMenu1->isVisible()); + + // navigate within the sub-sub-menu + QQuickMenuItem *subSubMenuItem1 = qobject_cast<QQuickMenuItem *>(subSubMenu1->itemAt(0)); + QVERIFY(subSubMenuItem1); + QQuickMenuItem *subSubMenuItem2 = qobject_cast<QQuickMenuItem *>(subSubMenu1->itemAt(1)); + QVERIFY(subSubMenuItem2); + QVERIFY(subSubMenuItem1->isHighlighted()); + QVERIFY(!subSubMenuItem2->isHighlighted()); + QTest::keyClick(window, Qt::Key_Down); + QVERIFY(!subSubMenuItem1->isHighlighted()); + QVERIFY(subSubMenuItem2->isHighlighted()); + + // navigate to the parent menu with the arrow key + QTest::keyClick(window, mirrored ? Qt::Key_Right : Qt::Key_Left); + QVERIFY(subSubMenu1Item->isHighlighted()); + QCOMPARE(mainMenu->isVisible(), cascade); + QVERIFY(subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + + // navigate within the sub-menu + QQuickMenuItem *subMenuItem1 = qobject_cast<QQuickMenuItem *>(subMenu1->itemAt(0)); + QVERIFY(subMenuItem1); + QQuickMenuItem *subMenuItem2 = qobject_cast<QQuickMenuItem *>(subMenu1->itemAt(1)); + QVERIFY(subMenuItem2); + QVERIFY(!subMenuItem1->isHighlighted()); + QVERIFY(!subMenuItem2->isHighlighted()); + QVERIFY(subSubMenu1Item->isHighlighted()); + QTest::keyClick(window, Qt::Key_Up); + QVERIFY(!subMenuItem1->isHighlighted()); + QVERIFY(subMenuItem2->isHighlighted()); + QVERIFY(!subSubMenu1Item->isHighlighted()); + + // close the menus with esc + QTest::keyClick(window, Qt::Key_Escape); + QCOMPARE(mainMenu->isVisible(), cascade); + QVERIFY(!subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + QTest::keyClick(window, Qt::Key_Escape); + QVERIFY(!mainMenu->isVisible()); + QVERIFY(!subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); +} + +void tst_menu::subMenuPosition_data() +{ + QTest::addColumn<bool>("cascade"); + QTest::addColumn<bool>("mirrored"); + QTest::addColumn<qreal>("overlap"); + + QTest::newRow("cascading") << true << false << 0.0; + QTest::newRow("cascading,overlap") << true << false << 10.0; + QTest::newRow("cascading,mirrored") << true << true << 0.0; + QTest::newRow("cascading,mirrored,overlap") << true << true << 10.0; + QTest::newRow("non-cascading") << false << false << 0.0; +} + +void tst_menu::subMenuPosition() +{ + QFETCH(bool, cascade); + QFETCH(bool, mirrored); + QFETCH(qreal, overlap); + + QQuickApplicationHelper helper(this, QLatin1String("subMenus.qml")); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + QVERIFY(QTest::qWaitForWindowActive(window)); + moveMouseAway(window); + + if (mirrored) + window->setLocale(QLocale("ar_EG")); + + QQuickMenu *mainMenu = window->property("mainMenu").value<QQuickMenu *>(); + QVERIFY(mainMenu); + mainMenu->setCascade(cascade); + QCOMPARE(mainMenu->cascade(), cascade); + mainMenu->setOverlap(overlap); + QCOMPARE(mainMenu->overlap(), overlap); + + QQuickMenu *subMenu1 = window->property("subMenu1").value<QQuickMenu *>(); + QVERIFY(subMenu1); + subMenu1->setCascade(cascade); + QCOMPARE(subMenu1->cascade(), cascade); + subMenu1->setOverlap(overlap); + QCOMPARE(subMenu1->overlap(), overlap); + + QQuickMenu *subMenu2 = window->property("subMenu2").value<QQuickMenu *>(); + QVERIFY(subMenu2); + subMenu2->setCascade(cascade); + QCOMPARE(subMenu2->cascade(), cascade); + subMenu2->setOverlap(overlap); + QCOMPARE(subMenu2->overlap(), overlap); + + QQuickMenu *subSubMenu1 = window->property("subSubMenu1").value<QQuickMenu *>(); + QVERIFY(subSubMenu1); + subSubMenu1->setCascade(cascade); + QCOMPARE(subSubMenu1->cascade(), cascade); + subSubMenu1->setOverlap(overlap); + QCOMPARE(subSubMenu1->overlap(), overlap); + + if (mirrored) + mainMenu->setPosition(QPointF(290, 10)); + else + mainMenu->setPosition(QPointF(10, 10)); + + mainMenu->open(); + QVERIFY(mainMenu->isVisible()); + QVERIFY(!subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + + // open the sub-menu + QQuickMenuItem *subMenu1Item = qobject_cast<QQuickMenuItem *>(mainMenu->itemAt(1)); + QVERIFY(subMenu1Item); + QCOMPARE(subMenu1Item->subMenu(), subMenu1); + emit subMenu1Item->triggered(); + QCOMPARE(mainMenu->isVisible(), cascade); + QVERIFY(subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + + if (cascade) { + QCOMPARE(subMenu1->parentItem(), subMenu1Item); + // vertically aligned to the parent menu item + QCOMPARE(subMenu1->popupItem()->y(), mainMenu->popupItem()->y() + subMenu1Item->y() - subMenu1->topPadding()); + if (mirrored) + QCOMPARE(subMenu1->popupItem()->x(), mainMenu->popupItem()->x() - subMenu1->width() + overlap); // on the left of the parent menu + else + QCOMPARE(subMenu1->popupItem()->x(), mainMenu->popupItem()->x() + mainMenu->width() - overlap); // on the right of the parent menu + } else { + QCOMPARE(subMenu1->parentItem(), mainMenu->parentItem()); + // centered over the parent menu + QCOMPARE(subMenu1->popupItem()->x(), mainMenu->popupItem()->x() + (mainMenu->width() - subMenu1->width()) / 2); + QCOMPARE(subMenu1->popupItem()->y(), mainMenu->popupItem()->y() + (mainMenu->height() - subMenu1->height()) / 2); + } + + // open the sub-sub-menu + QQuickMenuItem *subSubMenu1Item = qobject_cast<QQuickMenuItem *>(subMenu1->itemAt(2)); + QVERIFY(subSubMenu1Item); + QCOMPARE(subSubMenu1Item->subMenu(), subSubMenu1); + emit subSubMenu1Item->triggered(); + QCOMPARE(mainMenu->isVisible(), cascade); + QCOMPARE(subMenu1->isVisible(), cascade); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(subSubMenu1->isVisible()); + + if (cascade) { + QCOMPARE(subSubMenu1->parentItem(), subSubMenu1Item); + // vertically aligned to the parent menu item + QCOMPARE(subSubMenu1->popupItem()->y(), subMenu1->popupItem()->y() + subSubMenu1Item->y() - subSubMenu1->topPadding()); + if (mirrored) + QCOMPARE(subSubMenu1->popupItem()->x(), subMenu1->popupItem()->x() - subSubMenu1->width() + overlap); // on the left of the parent menu + else + QCOMPARE(subSubMenu1->popupItem()->x(), subMenu1->popupItem()->x() + subMenu1->width() - overlap); // on the right of the parent menu + } else { + QCOMPARE(subSubMenu1->parentItem(), subMenu1->parentItem()); + // centered over the parent menu + QCOMPARE(subSubMenu1->popupItem()->x(), subMenu1->popupItem()->x() + (subMenu1->width() - subSubMenu1->width()) / 2); + QCOMPARE(subSubMenu1->popupItem()->y(), subMenu1->popupItem()->y() + (subMenu1->height() - subSubMenu1->height()) / 2); + } +} + QTEST_MAIN(tst_menu) #include "tst_menu.moc" |