aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJ-P Nurmi <jpnurmi@qt.io>2017-06-01 23:36:57 +0200
committerJ-P Nurmi <jpnurmi@qt.io>2017-06-13 09:34:39 +0000
commit92fcd0a3fb2ee0d1ba242607c1aa5a29864d3650 (patch)
treee55ace5afc75cfaa17f2571fda8cfae2d8f4e9f8
parent23a80af1b5d31621bf1d342706c9541dd5273b86 (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>
-rw-r--r--src/imports/controls/Menu.qml1
-rw-r--r--src/imports/controls/MenuItem.qml15
-rw-r--r--src/imports/controls/fusion/Menu.qml1
-rw-r--r--src/imports/controls/fusion/MenuItem.qml18
-rw-r--r--src/imports/controls/images/arrow-indicator.pngbin0 -> 260 bytes
-rw-r--r--src/imports/controls/images/arrow-indicator@2x.pngbin0 -> 343 bytes
-rw-r--r--src/imports/controls/images/arrow-indicator@3x.pngbin0 -> 438 bytes
-rw-r--r--src/imports/controls/images/arrow-indicator@4x.pngbin0 -> 545 bytes
-rw-r--r--src/imports/controls/material/MenuItem.qml16
-rw-r--r--src/imports/controls/material/images/arrow-indicator.pngbin0 -> 200 bytes
-rw-r--r--src/imports/controls/material/images/arrow-indicator.svg56
-rw-r--r--src/imports/controls/material/images/arrow-indicator@2x.pngbin0 -> 239 bytes
-rw-r--r--src/imports/controls/material/images/arrow-indicator@3x.pngbin0 -> 277 bytes
-rw-r--r--src/imports/controls/material/images/arrow-indicator@4x.pngbin0 -> 317 bytes
-rw-r--r--src/imports/controls/material/qtquickcontrols2materialstyleplugin.qrc4
-rw-r--r--src/imports/controls/qtquickcontrols2plugin.qrc4
-rw-r--r--src/imports/controls/universal/Menu.qml1
-rw-r--r--src/imports/controls/universal/MenuItem.qml16
-rw-r--r--src/quicktemplates2/qquickmenu.cpp273
-rw-r--r--src/quicktemplates2/qquickmenu_p.h10
-rw-r--r--src/quicktemplates2/qquickmenu_p_p.h21
-rw-r--r--src/quicktemplates2/qquickmenuitem.cpp64
-rw-r--r--src/quicktemplates2/qquickmenuitem_p.h8
-rw-r--r--src/quicktemplates2/qquickmenuitem_p_p.h3
-rw-r--r--tests/auto/menu/data/subMenus.qml96
-rw-r--r--tests/auto/menu/tst_menu.cpp341
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
new file mode 100644
index 00000000..50f230dc
--- /dev/null
+++ b/src/imports/controls/images/arrow-indicator.png
Binary files differ
diff --git a/src/imports/controls/images/arrow-indicator@2x.png b/src/imports/controls/images/arrow-indicator@2x.png
new file mode 100644
index 00000000..457cdde0
--- /dev/null
+++ b/src/imports/controls/images/arrow-indicator@2x.png
Binary files differ
diff --git a/src/imports/controls/images/arrow-indicator@3x.png b/src/imports/controls/images/arrow-indicator@3x.png
new file mode 100644
index 00000000..8d624154
--- /dev/null
+++ b/src/imports/controls/images/arrow-indicator@3x.png
Binary files differ
diff --git a/src/imports/controls/images/arrow-indicator@4x.png b/src/imports/controls/images/arrow-indicator@4x.png
new file mode 100644
index 00000000..7d2c49e2
--- /dev/null
+++ b/src/imports/controls/images/arrow-indicator@4x.png
Binary files differ
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
new file mode 100644
index 00000000..4a942849
--- /dev/null
+++ b/src/imports/controls/material/images/arrow-indicator.png
Binary files differ
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
new file mode 100644
index 00000000..b31b56c1
--- /dev/null
+++ b/src/imports/controls/material/images/arrow-indicator@2x.png
Binary files differ
diff --git a/src/imports/controls/material/images/arrow-indicator@3x.png b/src/imports/controls/material/images/arrow-indicator@3x.png
new file mode 100644
index 00000000..637e9674
--- /dev/null
+++ b/src/imports/controls/material/images/arrow-indicator@3x.png
Binary files differ
diff --git a/src/imports/controls/material/images/arrow-indicator@4x.png b/src/imports/controls/material/images/arrow-indicator@4x.png
new file mode 100644
index 00000000..15b9a902
--- /dev/null
+++ b/src/imports/controls/material/images/arrow-indicator@4x.png
Binary files differ
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"