diff options
Diffstat (limited to 'tests/auto/quickcontrols/qquickmenu')
26 files changed, 3194 insertions, 0 deletions
diff --git a/tests/auto/quickcontrols/qquickmenu/BLACKLIST b/tests/auto/quickcontrols/qquickmenu/BLACKLIST new file mode 100644 index 0000000000..fed4c6e561 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/BLACKLIST @@ -0,0 +1,7 @@ +# QTBUG-98491 +[disableWhenTriggered] +macos + +# QTBUG-98491 +[subMenuPosition] +macos diff --git a/tests/auto/quickcontrols/qquickmenu/CMakeLists.txt b/tests/auto/quickcontrols/qquickmenu/CMakeLists.txt new file mode 100644 index 0000000000..84499748b1 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/CMakeLists.txt @@ -0,0 +1,57 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Generated from qquickmenu.pro. + +if (NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qquickmenu LANGUAGES C CXX ASM) + find_package(Qt6BuildInternals COMPONENTS STANDALONE_TEST) +endif() + +##################################################################### +## tst_qquickmenu Test: +##################################################################### + +# Collect test data +file(GLOB_RECURSE test_data_glob + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + data/*) +list(APPEND test_data ${test_data_glob}) + +qt_internal_add_test(tst_qquickmenu + SOURCES + tst_qquickmenu.cpp + DEFINES + QQC2_IMPORT_PATH=\\\"${CMAKE_CURRENT_SOURCE_DIR}/../../../../src/quickcontrols\\\" + LIBRARIES + Qt::CorePrivate + Qt::Gui + Qt::GuiPrivate + Qt::QmlPrivate + Qt::QuickControls2 + Qt::QuickControls2Private + Qt::QuickControlsTestUtilsPrivate + Qt::QuickPrivate + Qt::QuickTemplates2Private + Qt::QuickTest + Qt::QuickTestUtilsPrivate + Qt::TestPrivate + TESTDATA ${test_data} +) + +#### Keys ignored in scope 1:.:.:qquickmenu.pro:<TRUE>: +# OTHER_FILES = "data/*.qml" + +## Scopes: +##################################################################### + +qt_internal_extend_target(tst_qquickmenu CONDITION ANDROID OR IOS + DEFINES + QT_QMLTEST_DATADIR=\\\":/data\\\" +) + +qt_internal_extend_target(tst_qquickmenu CONDITION NOT ANDROID AND NOT IOS + DEFINES + QT_QMLTEST_DATADIR=\\\"${CMAKE_CURRENT_SOURCE_DIR}/data\\\" +) diff --git a/tests/auto/quickcontrols/qquickmenu/data/actionShortcuts.qml b/tests/auto/quickcontrols/qquickmenu/data/actionShortcuts.qml new file mode 100644 index 0000000000..3d839b98d4 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/actionShortcuts.qml @@ -0,0 +1,50 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 400 + height: 400 + + property alias menu: menu + property alias subMenu: subMenu + property alias buttonMenu: buttonMenu + + Menu { + id: menu + objectName: "menu" + + Action { + objectName: text + text: "action1" + shortcut: "A" + } + + Menu { + id: subMenu + objectName: "subMenu" + + Action { + objectName: text + text: "subAction1" + shortcut: "B" + } + } + } + + Button { + text: "Menu button" + + Menu { + id: buttonMenu + + Action { + objectName: text + text: "buttonMenuAction1" + shortcut: "C" + } + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/actions.qml b/tests/auto/quickcontrols/qquickmenu/data/actions.qml new file mode 100644 index 0000000000..0c4449d888 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/actions.qml @@ -0,0 +1,20 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 400 + height: 400 + + property alias menu: menu + + Menu { + id: menu + Action { text: "action1" } + MenuItem { text: "menuitem2" } + Action { text: "action3" } + MenuItem { text: "menuitem4" } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/addItem.qml b/tests/auto/quickcontrols/qquickmenu/data/addItem.qml new file mode 100644 index 0000000000..5678210e25 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/addItem.qml @@ -0,0 +1,24 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 200 + height: 200 + + property alias menu: menu + + MenuItem { + id: newMenuItem + text: qsTr("New") + } + + Menu { + id: menu + y: parent.height + + Component.onCompleted: addItem(newMenuItem) + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/applicationWindowScrollable.qml b/tests/auto/quickcontrols/qquickmenu/data/applicationWindowScrollable.qml new file mode 100644 index 0000000000..eb8140eea5 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/applicationWindowScrollable.qml @@ -0,0 +1,26 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + title: "Test Application Window" + width: 300 + height: 300 + + property alias menu: menu + + Menu { + id: menu + + Repeater { + model: 20 + + delegate: MenuItem { + objectName: text + text: (index + 1) + } + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/applicationwindow.qml b/tests/auto/quickcontrols/qquickmenu/data/applicationwindow.qml new file mode 100644 index 0000000000..1b42ee6965 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/applicationwindow.qml @@ -0,0 +1,46 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + title: "Test Application Window" + width: 400 + height: 400 + + property alias emptyMenu: emptyMenu + property alias menu: menu + property alias menuButton: menuButton + property Overlay overlay: menu.Overlay.overlay + + Menu { + id: emptyMenu + } + + Menu { + id: menu + cascade: true + + MenuItem { + objectName: "firstMenuItem" + text: "A" + } + MenuItem { + objectName: "secondMenuItem" + text: "B" + } + MenuItem { + objectName: "thirdMenuItem" + text: "C" + } + } + + Button { + id: menuButton + x: 250 + visible: false + text: "Open Menu" + onClicked: menu.open() + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/customMenuCullItems.qml b/tests/auto/quickcontrols/qquickmenu/data/customMenuCullItems.qml new file mode 100644 index 0000000000..0f56ecdd87 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/customMenuCullItems.qml @@ -0,0 +1,55 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +ApplicationWindow { + width: 200 + height: 200 + property alias menu: menu + + Menu { + id: menu + + contentItem: FocusScope { + implicitHeight: view.implicitHeight + Button { + anchors { + top: parent.top + topMargin: 5 + horizontalCenter: parent.horizontalCenter + } + z: 1 + text: "Button Up" + visible: view.interactive + } + ListView { + id: view + width: parent.width + implicitHeight: Math.min(contentHeight, 300) + model: menu.contentModel + + clip: true + currentIndex: menu.currentIndex + ScrollIndicator.vertical: ScrollIndicator {} + } + Button { + anchors { + bottom: parent.bottom + bottomMargin: 5 + horizontalCenter: parent.horizontalCenter + } + z: 1 + text: "Button Down" + visible: view.interactive + } + } + + Repeater { + model: 20 + MenuItem { + objectName: "Item: " + modelData + text: objectName + } + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/customMenuUseRepeaterAsTheContentItem.qml b/tests/auto/quickcontrols/qquickmenu/data/customMenuUseRepeaterAsTheContentItem.qml new file mode 100644 index 0000000000..bfa8f66be5 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/customMenuUseRepeaterAsTheContentItem.qml @@ -0,0 +1,65 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +ApplicationWindow { + width: 200 + height: 200 + property alias menu: menu + + Menu { + id: menu + visible: true + + contentItem: FocusScope { + implicitHeight: flickable.height + + Button { + anchors { + top: parent.top + topMargin: 5 + horizontalCenter: parent.horizontalCenter + } + z: 1 + text: "Button Up" + } + + Flickable { + id: flickable + width: parent.width + height: Math.min(contentHeight, 300) + contentHeight: repeaterLayout.implicitHeight + clip: true + + ScrollIndicator.vertical: ScrollIndicator {} + + ColumnLayout { + id: repeaterLayout + width: parent.width + + Repeater { + model: menu.contentModel + } + } + } + + Button { + anchors { + bottom: parent.bottom + bottomMargin: 5 + horizontalCenter: parent.horizontalCenter + } + z: 1 + text: "Button Down" + } + } + + Repeater { + model: 20 + MenuItem { + objectName: "Item: " + modelData + text: objectName + } + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/delegateFromSeparateComponent.qml b/tests/auto/quickcontrols/qquickmenu/data/delegateFromSeparateComponent.qml new file mode 100644 index 0000000000..44a7d8d54a --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/delegateFromSeparateComponent.qml @@ -0,0 +1,60 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 800 + height: 800 + + property alias menu: menu + + Component { + id: menuItemComponent + + MenuItem { + contentItem: Text { + text: parent.text + color: "blue" + } + background: Rectangle { + color: "#00ff00" + } + } + } + + Menu { + id: menu + title: "Root Menu" + + Action { + text: "Action Item 1" + } + Menu { + title: "Sub-menu" + delegate: menuItemComponent + + Action { + text: "Sub-menu Action Item 1" + } + Menu { + title: "Sub-sub-menu" + delegate: menuItemComponent + + Action { + text: "Sub-sub-menu Action Item 1" + } + } + Action { + text: "Sub-menu Action Item 2" + } + } + Action { + text: "Action Item 2" + } + + delegate: menuItemComponent + visible: true + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/disableWhenTriggered.qml b/tests/auto/quickcontrols/qquickmenu/data/disableWhenTriggered.qml new file mode 100644 index 0000000000..e59026677a --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/disableWhenTriggered.qml @@ -0,0 +1,74 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 400 + height: 400 + + Action { + id: actionOutsideMenu + text: "Action declared outside menu" + onTriggered: enabled = false + } + + menuBar: MenuBar { + Menu { + title: "Menu" + objectName: title + + Action { + text: "Action" + objectName: text + onTriggered: enabled = false + } + MenuItem { + objectName: "MenuItem with Action" + action: Action { + text: "Action declared inside MenuItem" + objectName: text + onTriggered: enabled = false + } + } + MenuItem { + objectName: "MenuItem with Action declared outside menu" + action: actionOutsideMenu + } + MenuItem { + text: "MenuItem with no Action" + objectName: text + onTriggered: enabled = false + } + + Menu { + title: "Submenu" + objectName: title + + Action { + text: "Sub-Action" + objectName: text + onTriggered: enabled = false + } + MenuItem { + objectName: "Sub-MenuItem with Action declared inside" + action: Action { + text: "Action declared inside Sub-MenuItem" + objectName: text + onTriggered: enabled = false + } + } + MenuItem { + objectName: "Sub-MenuItem with Action declared outside menu" + action: actionOutsideMenu + } + MenuItem { + text: "Sub-MenuItem with no Action" + objectName: text + onTriggered: enabled = false + } + } + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/disabledMenuItemKeyNavigation.qml b/tests/auto/quickcontrols/qquickmenu/data/disabledMenuItemKeyNavigation.qml new file mode 100644 index 0000000000..c9c893bb9c --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/disabledMenuItemKeyNavigation.qml @@ -0,0 +1,27 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 200 + height: 200 + + property alias menu: menu + + Menu { + id: menu + + MenuItem { + text: qsTr("Enabled 1") + } + MenuItem { + text: qsTr("Disabled 1") + enabled: false + } + MenuItem { + text: qsTr("Enabled 2") + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/giveMenuItemFocusOnButtonPress.qml b/tests/auto/quickcontrols/qquickmenu/data/giveMenuItemFocusOnButtonPress.qml new file mode 100644 index 0000000000..1db3e351bc --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/giveMenuItemFocusOnButtonPress.qml @@ -0,0 +1,50 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 640 + height: 480 + + property alias menuButton: menuButton + property alias menu: menu + + signal menuButtonClicked + + Button { + id: menuButton + text: "Open menu" + + // Buttons do not emit clicked() for enter/return, hence the Keys usage. + // The signal is just for the test to ensure that the return was actually handled. + Keys.onReturnPressed: { + menuButtonClicked() + menu.open() + } + } + + Menu { + id: menu + parent: menuButton + + onOpened: command1.forceActiveFocus() + + MenuItem { + id: command1 + objectName: text + text: "Command 1" + } + + MenuItem { + objectName: text + text: "Command 2" + } + + MenuItem { + objectName: text + text: "Command 3" + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/menuItemWidths.qml b/tests/auto/quickcontrols/qquickmenu/data/menuItemWidths.qml new file mode 100644 index 0000000000..ff99abd399 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/menuItemWidths.qml @@ -0,0 +1,56 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 600 + height: 600 + + property alias menu: menu + + Menu { + id: menu + MenuItem { + objectName: "MenuItem" + text: "MenuItem" + } + MenuSeparator { + objectName: "MenuSeparator" + } + Menu { + title: "Sub-menu" + objectName: "Sub-menu" + + MenuItem { + objectName: "SubMenuItem" + text: "SubMenuItem" + } + } + Rectangle { + objectName: "CustomSeparator" + height: 2 + color: "salmon" + } + Rectangle { + // Use a binding to test retranslate(), which re-evaluates all bindings. + implicitWidth: someValue + objectName: "CustomRectangleSeparator" + height: 2 + color: "salmon" + + property int someValue: 120 + } + Control { + objectName: "CustomControlSeparator" + implicitWidth: someOtherValue + height: 2 + background: Rectangle { + color: "navajowhite" + } + + property int someOtherValue: 180 + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/menuSeparator.qml b/tests/auto/quickcontrols/qquickmenu/data/menuSeparator.qml new file mode 100644 index 0000000000..b13cd534fd --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/menuSeparator.qml @@ -0,0 +1,37 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 200 + height: 200 + + property alias menu: menu + + MenuItem { + id: newMenuItem + text: qsTr("New") + } + + MenuSeparator { + id: menuSeparator + } + + MenuItem { + id: saveMenuItem + text: qsTr("Save") + } + + Menu { + id: menu + cascade: true + + Component.onCompleted: { + addItem(newMenuItem) + addItem(menuSeparator) + addItem(saveMenuItem) + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/mnemonics.qml b/tests/auto/quickcontrols/qquickmenu/data/mnemonics.qml new file mode 100644 index 0000000000..8929b00275 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/mnemonics.qml @@ -0,0 +1,40 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 400 + height: 400 + + property alias menu: menu + property alias action: action + property alias menuItem: menuItem + property alias subMenu: subMenu + property alias subMenuItem: subMenuItem + + Menu { + id: menu + + Action { + id: action + text: "&Action" + } + + MenuItem { + id: menuItem + text: "Menu &Item" + } + + Menu { + id: subMenu + title: "Sub &Menu" + + MenuItem { + id: subMenuItem + text: "&Sub Menu Item" + } + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/order.qml b/tests/auto/quickcontrols/qquickmenu/data/order.qml new file mode 100644 index 0000000000..185c9e45c3 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/order.qml @@ -0,0 +1,34 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 200 + height: 200 + + property alias menu: menu + + Component { + id: menuItem + MenuItem { } + } + + Menu { + id: menu + property alias repeater: repeater + MenuItem { text: "static_1" } + Repeater { + id: repeater + model: 2 + MenuItem { text: "repeated_" + (index + 2) } + } + MenuItem { text: "static_4" } + Component.onCompleted: { + addItem(menuItem.createObject(menu.contentItem, {text: "dynamic_5"})) + addItem(menuItem.createObject(menu.contentItem, {text: "dynamic_6"})) + insertItem(0, menuItem.createObject(menu.contentItem, {text: "dynamic_0"})) + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/popup.qml b/tests/auto/quickcontrols/qquickmenu/data/popup.qml new file mode 100644 index 0000000000..8201c9e03f --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/popup.qml @@ -0,0 +1,77 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 500 + height: 600 + + property alias menu: menu + property alias menuItem1: menuItem1 + property alias menuItem2: menuItem2 + property alias menuItem3: menuItem3 + property alias button: button + + function popupAtCursor() { + menu.popup() + } + + function popupAtPos(pos) { + menu.popup(pos) + } + + function popupAtCoord(x, y) { + menu.popup(x, y) + } + + function popupItemAtCursor(item) { + menu.popup(item) + } + + function popupItemAtPos(pos, item) { + menu.popup(pos, item) + } + + function popupItemAtCoord(x, y, item) { + menu.popup(x, y, item) + } + + function popupAtParentCursor(parent) { + menu.popup(parent) + } + + function popupAtParentPos(parent, pos) { + menu.popup(parent, pos) + } + + function popupAtParentCoord(parent, x, y) { + menu.popup(parent, x, y) + } + + function popupItemAtParentCursor(parent, item) { + menu.popup(parent, item) + } + + function popupItemAtParentPos(parent, pos, item) { + menu.popup(parent, pos, item) + } + + function popupItemAtParentCoord(parent, x, y, item) { + menu.popup(parent, x, y, item) + } + + Menu { + id: menu + MenuItem { id: menuItem1; text: "Foo" } + MenuItem { id: menuItem2; text: "Bar" } + MenuItem { id: menuItem3; text: "Baz" } + } + + Button { + id: button + text: "Button" + anchors.centerIn: parent + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/pressAndHold.qml b/tests/auto/quickcontrols/qquickmenu/data/pressAndHold.qml new file mode 100644 index 0000000000..ac0f394604 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/pressAndHold.qml @@ -0,0 +1,26 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 400 + height: 400 + + property alias menu: menu + + MouseArea { + anchors.fill: parent + onPressAndHold: menu.open() + } + + Menu { + id: menu + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + MenuItem { text: "One" } + MenuItem { text: "Two" } + MenuItem { text: "Three" } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/removeTakeItem.qml b/tests/auto/quickcontrols/qquickmenu/data/removeTakeItem.qml new file mode 100644 index 0000000000..aa321cb3a9 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/removeTakeItem.qml @@ -0,0 +1,40 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 200 + height: 200 + + property alias menu: menu + property alias menuItem1: menuItem1 + property alias menuItem2: menuItem2 + property alias menuItem3: menuItem3 + + function takeSecondItem() { + return menu.takeItem(1) + } + + function removeFirstItem() { + menu.removeItem(menuItem1) + } + + function removeNullItem() { + menu.removeItem(null) + } + + Menu { + id: menu + MenuItem { + id: menuItem1 + } + MenuItem { + id: menuItem2 + } + MenuItem { + id: menuItem3 + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/repeater.qml b/tests/auto/quickcontrols/qquickmenu/data/repeater.qml new file mode 100644 index 0000000000..3c056a1d38 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/repeater.qml @@ -0,0 +1,22 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 200 + height: 200 + + property alias menu: menu + property alias repeater: repeater + + Menu { + id: menu + Repeater { + id: repeater + model: 5 + MenuItem { property int idx: index } + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/scrollableWithPadding.qml b/tests/auto/quickcontrols/qquickmenu/data/scrollableWithPadding.qml new file mode 100644 index 0000000000..842cb33bfd --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/scrollableWithPadding.qml @@ -0,0 +1,32 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Window + +Window { + title: "Test Window" + width: 300 + height: dummyItem.height * 10 + + property alias menu: menu + MenuItem { + id: dummyItem + objectName: "Dummy" + text: objectName + } + + Menu { + id: menu + topPadding: 10 + Repeater { + model: 10 + + delegate: MenuItem { + objectName: text + text: (index + 1) + } + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/subMenuDisabled.qml b/tests/auto/quickcontrols/qquickmenu/data/subMenuDisabled.qml new file mode 100644 index 0000000000..4d5db0bd67 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/subMenuDisabled.qml @@ -0,0 +1,32 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 600 + height: 400 + + property alias mainMenu: mainMenu + property alias subMenu: subMenu + + Menu { + id: mainMenu + title: "Menu" + + Menu { + id: subMenu + title: "Sub Menu" + MenuItem { + id: subMenuItem1 + text: "Sub Menu Item 1" + enabled: false + } + MenuItem { + id: subMenuItem2 + text: "Sub Menu Item 2" + } + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/subMenus.qml b/tests/auto/quickcontrols/qquickmenu/data/subMenus.qml new file mode 100644 index 0000000000..280fd404e8 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/subMenus.qml @@ -0,0 +1,90 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + width: 600 + height: 400 + + property alias mainMenu: mainMenu + property alias subMenu1: subMenu1 + property alias subMenu2: subMenu2 + property alias subSubMenu1: subSubMenu1 + + Menu { + id: mainMenu + MenuItem { + id: mainMenuItem1 + objectName: "mainMenuItem1" + text: "Main 1" + } + + Menu { + overlap: 0 + id: subMenu1 + objectName: "subMenu1" + title: "Sub Menu 1" + + MenuItem { + id: subMenuItem1 + objectName: "subMenuItem1" + text: "Sub 1" + } + + MenuItem { + id: subMenuItem2 + objectName: "subMenuItem2" + text: "Sub 2" + } + + Menu { + overlap: 0 + id: subSubMenu1 + objectName: "subSubMenu1" + title: "Sub Sub Menu 1" + + MenuItem { + id: subSubMenuItem1 + objectName: "subSubMenuItem1" + text: "Sub Sub 1" + } + MenuItem { + id: subSubMenuItem2 + objectName: "subSubMenuItem2" + text: "Sub Sub 2" + } + } + } + + MenuItem { + id: mainMenuItem2 + objectName: "mainMenuItem2" + text: "Main 2" + } + + Menu { + id: subMenu2 + objectName: "subMenu2" + title: "Sub Menu 2" + + MenuItem { + id: subMenuItem3 + objectName: "subMenuItem3" + text: "Sub 3" + } + MenuItem { + id: subMenuItem4 + objectName: "subMenuItem4" + text: "Sub 4" + } + } + + MenuItem { + id: mainMenuItem3 + objectName: "mainMenuItem3" + text: "Main 3" + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/data/windowScrollable.qml b/tests/auto/quickcontrols/qquickmenu/data/windowScrollable.qml new file mode 100644 index 0000000000..97a06da63d --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/windowScrollable.qml @@ -0,0 +1,27 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Window + +Window { + title: "Test Window" + width: 300 + height: 300 + + property alias menu: menu + + Menu { + id: menu + + Repeater { + model: 20 + + delegate: MenuItem { + objectName: text + text: (index + 1) + } + } + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp b/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp new file mode 100644 index 0000000000..6a1d1ab803 --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp @@ -0,0 +1,2120 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QtTest/qtest.h> +#include <QtTest/qsignalspy.h> +#include <QtGui/qcursor.h> +#if QT_CONFIG(shortcut) +#include <QtGui/qkeysequence.h> +#endif +#include <QtGui/qstylehints.h> +#include <QtGui/qpa/qplatformintegration.h> +#include <QtGui/private/qguiapplication_p.h> +#include <QtQml/qqmlengine.h> +#include <QtQml/qqmlcomponent.h> +#include <QtQml/qqmlcontext.h> +#include <QtQuick/qquickview.h> +#include <QtQuick/private/qquickitem_p.h> +#include <QtQuickTestUtils/private/qmlutils_p.h> +#include <QtQuickTestUtils/private/visualtestutils_p.h> +#include <QtQuickControlsTestUtils/private/controlstestutils_p.h> +#include <QtQuickControlsTestUtils/private/qtest_quickcontrols_p.h> + +#include <QtQuickTemplates2/private/qquickaction_p.h> +#include <QtQuickTemplates2/private/qquickapplicationwindow_p.h> +#include <QtQuickTemplates2/private/qquickoverlay_p.h> +#include <QtQuickTemplates2/private/qquickbutton_p.h> +#include <QtQuickTemplates2/private/qquickicon_p.h> +#include <QtQuickTemplates2/private/qquickmenu_p.h> +#include <QtQuickTemplates2/private/qquickmenuitem_p.h> +#include <QtQuickTemplates2/private/qquickmenuseparator_p.h> + +using namespace QQuickVisualTestUtils; +using namespace QQuickControlsTestUtils; + +class tst_QQuickMenu : public QQmlDataTest +{ + Q_OBJECT + +public: + tst_QQuickMenu(); + +private slots: + void defaults(); + void count(); + void mouse(); + void pressAndHold(); + void contextMenuKeyboard(); + void disabledMenuItemKeyNavigation(); + void mnemonics(); + void menuButton(); + void addItem(); + void menuSeparator(); + void repeater(); + void order(); +#if QT_CONFIG(cursor) + void popup(); +#endif + void actions(); +#if QT_CONFIG(shortcut) + void actionShortcuts(); +#endif + void removeTakeItem(); + void subMenuMouse_data(); + void subMenuMouse(); + void subMenuDisabledMouse_data(); + void subMenuDisabledMouse(); + void subMenuKeyboard_data(); + void subMenuKeyboard(); + void subMenuDisabledKeyboard_data(); + void subMenuDisabledKeyboard(); + void subMenuPosition_data(); + void subMenuPosition(); + void subMenuWithIcon(); + void addRemoveSubMenus(); + void scrollable_data(); + void scrollable(); + void disableWhenTriggered_data(); + void disableWhenTriggered(); + void menuItemWidth_data(); + void menuItemWidth(); + void menuItemWidthAfterMenuWidthChanged_data(); + void menuItemWidthAfterMenuWidthChanged(); + void menuItemWidthAfterImplicitWidthChanged_data(); + void menuItemWidthAfterImplicitWidthChanged(); + void menuItemWidthAfterRetranslate(); + void giveMenuItemFocusOnButtonPress(); + void customMenuCullItems(); + void customMenuUseRepeaterAsTheContentItem(); + +private: + static bool hasWindowActivation(); +}; + +tst_QQuickMenu::tst_QQuickMenu() + : QQmlDataTest(QT_QMLTEST_DATADIR) +{ +} + +bool tst_QQuickMenu::hasWindowActivation() +{ + return (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowActivation)); +} + +void tst_QQuickMenu::defaults() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("applicationwindow.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + + QQuickMenu *emptyMenu = helper.appWindow->property("emptyMenu").value<QQuickMenu*>(); + QCOMPARE(emptyMenu->isVisible(), false); + QVERIFY(emptyMenu->hasFocus()); + QCOMPARE(emptyMenu->currentIndex(), -1); + QCOMPARE(emptyMenu->contentItem()->property("currentIndex"), QVariant(-1)); + QCOMPARE(emptyMenu->count(), 0); +} + +void tst_QQuickMenu::count() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("applicationwindow.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + + QQuickMenu *menu = helper.window->property("emptyMenu").value<QQuickMenu*>(); + QVERIFY(menu); + + QSignalSpy countSpy(menu, &QQuickMenu::countChanged); + QVERIFY(countSpy.isValid()); + + menu->addItem(new QQuickItem); + QCOMPARE(menu->count(), 1); + QCOMPARE(countSpy.size(), 1); + + menu->insertItem(0, new QQuickItem); + QCOMPARE(menu->count(), 2); + QCOMPARE(countSpy.size(), 2); + + menu->removeItem(menu->itemAt(1)); + QCOMPARE(menu->count(), 1); + QCOMPARE(countSpy.size(), 3); + + QScopedPointer<QQuickItem> item(menu->takeItem(0)); + QVERIFY(item); + QCOMPARE(menu->count(), 0); + QCOMPARE(countSpy.size(), 4); +} + +void tst_QQuickMenu::mouse() +{ + if (!hasWindowActivation()) + QSKIP("Window activation is not supported"); + + if ((QGuiApplication::platformName() == QLatin1String("offscreen")) + || (QGuiApplication::platformName() == QLatin1String("minimal"))) + QSKIP("Mouse hovering not functional on offscreen/minimal platforms"); + + QQuickControlsApplicationHelper helper(this, QLatin1String("applicationwindow.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + + QQuickApplicationWindow *window = helper.appWindow; + centerOnScreen(window); + moveMouseAway(window); + window->show(); + QVERIFY(QTest::qWaitForWindowActive(window)); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu*>(); + QVERIFY(menu); + menu->open(); + QVERIFY(menu->isVisible()); + QQuickOverlay *overlay = window->property("overlay").value<QQuickOverlay*>(); + QVERIFY(overlay); + QVERIFY(overlay->childItems().contains(menu->contentItem()->parentItem())); + QTRY_VERIFY(menu->isOpened()); + + QQuickItem *firstItem = menu->itemAt(0); + QSignalSpy clickedSpy(firstItem, SIGNAL(clicked())); + QSignalSpy triggeredSpy(firstItem, SIGNAL(triggered())); + QSignalSpy visibleSpy(menu, SIGNAL(visibleChanged())); + + // Ensure that presses cause the current index to change, + // so that the highlight acts as a way of illustrating press state. + QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, + QPoint(menu->x() + menu->leftPadding() + firstItem->width() / 2, menu->y() + menu->topPadding() + firstItem->height() / 2)); + QVERIFY(firstItem->hasActiveFocus()); + QCOMPARE(menu->currentIndex(), 0); + QCOMPARE(menu->contentItem()->property("currentIndex"), QVariant(0)); + QVERIFY(menu->isVisible()); + + QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, + QPoint(menu->x() + menu->leftPadding() + firstItem->width() / 2, menu->y() + menu->topPadding() + firstItem->height() / 2)); + QCOMPARE(clickedSpy.size(), 1); + QCOMPARE(triggeredSpy.size(), 1); + QTRY_COMPARE(visibleSpy.size(), 1); + QVERIFY(!menu->isVisible()); + QVERIFY(!overlay->childItems().contains(menu->contentItem())); + QCOMPARE(menu->currentIndex(), -1); + QCOMPARE(menu->contentItem()->property("currentIndex"), QVariant(-1)); + + menu->open(); + QCOMPARE(visibleSpy.size(), 2); + QVERIFY(menu->isVisible()); + QVERIFY(overlay->childItems().contains(menu->contentItem()->parentItem())); + QTRY_VERIFY(menu->isOpened()); + + // Ensure that we have enough space to click outside of the menu. + QVERIFY(window->width() > menu->contentItem()->width()); + QVERIFY(window->height() > menu->contentItem()->height()); + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, + QPoint(menu->contentItem()->x() + menu->contentItem()->width() + 1, + menu->contentItem()->y() + menu->contentItem()->height() + 1)); + QTRY_COMPARE(visibleSpy.size(), 3); + QVERIFY(!menu->isVisible()); + QVERIFY(!overlay->childItems().contains(menu->contentItem()->parentItem())); + + menu->open(); + QCOMPARE(visibleSpy.size(), 4); + QVERIFY(menu->isVisible()); + QVERIFY(overlay->childItems().contains(menu->contentItem()->parentItem())); + QTRY_VERIFY(menu->isOpened()); + + // Hover-highlighting does not work on Android +#ifndef Q_OS_ANDROID + // Hover-highlight through the menu items one by one + QQuickItem *prevHoverItem = nullptr; + QQuickItem *listView = menu->contentItem(); + for (int y = menu->topPadding(); y < listView->height(); ++y) { + QQuickItem *hoverItem = nullptr; + QVERIFY(QMetaObject::invokeMethod(listView, "itemAt", Q_RETURN_ARG(QQuickItem *, hoverItem), Q_ARG(qreal, 0), Q_ARG(qreal, listView->property("contentY").toReal() + y))); + if (!hoverItem || !hoverItem->isVisible() || hoverItem == prevHoverItem) + continue; + QTest::mouseMove(window, QPoint( + menu->x() + menu->leftPadding() + hoverItem->x() + hoverItem->width() / 2, + menu->y() + menu->topPadding() + hoverItem->y() + hoverItem->height() / 2)); + QTRY_VERIFY(hoverItem->property("highlighted").toBool()); + if (prevHoverItem) + QVERIFY(!prevHoverItem->property("highlighted").toBool()); + prevHoverItem = hoverItem; + } +#endif + + // Try pressing within the menu and releasing outside of it; it should close. + // TODO: won't work until QQuickPopup::releasedOutside() actually gets emitted +// QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, QPoint(firstItem->width() / 2, firstItem->height() / 2)); +// QVERIFY(firstItem->hasActiveFocus()); +// QCOMPARE(menu->contentItem()->property("currentIndex"), QVariant(0)); +// QVERIFY(menu->isVisible()); +// QCOMPARE(triggeredSpy.count(), 1); + +// QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, QPoint(menu->contentItem()->width() + 1, firstItem->height() / 2)); +// QCOMPARE(clickedSpy.count(), 1); +// QCOMPARE(triggeredSpy.count(), 1); +// QCOMPARE(visibleSpy.count(), 5); +// QVERIFY(!menu->isVisible()); +// QVERIFY(!overlay->childItems().contains(menu->contentItem())); +// QCOMPARE(menu->contentItem()->property("currentIndex"), QVariant(-1)); +} + +void tst_QQuickMenu::pressAndHold() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("pressAndHold.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + + QQuickWindow *window = helper.window; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu *>(); + QVERIFY(menu); + + QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, QPoint(1, 1)); + QTRY_VERIFY(menu->isVisible()); + + QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, QPoint(1, 1)); + QVERIFY(menu->isVisible()); + + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, QPoint(1, 1)); + QTRY_VERIFY(!menu->isVisible()); +} + +void tst_QQuickMenu::contextMenuKeyboard() +{ + if (!hasWindowActivation()) + QSKIP("Window activation is not supported"); + + if (QGuiApplication::styleHints()->tabFocusBehavior() != Qt::TabFocusAllControls) + QSKIP("This platform only allows tab focus for text controls"); + + QQuickControlsApplicationHelper helper(this, QLatin1String("applicationwindow.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + + QQuickApplicationWindow *window = helper.appWindow; + centerOnScreen(window); + moveMouseAway(window); + window->show(); + window->requestActivate(); + QVERIFY(QTest::qWaitForWindowActive(window)); + QVERIFY(QGuiApplication::focusWindow() == window); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu*>(); + QCOMPARE(menu->currentIndex(), -1); + QCOMPARE(menu->contentItem()->property("currentIndex"), QVariant(-1)); + + QQuickMenuItem *firstItem = qobject_cast<QQuickMenuItem *>(menu->itemAt(0)); + QVERIFY(firstItem); + QSignalSpy visibleSpy(menu, SIGNAL(visibleChanged())); + + QVERIFY(menu->hasFocus()); + menu->open(); + QCOMPARE(visibleSpy.size(), 1); + QVERIFY(menu->isVisible()); + QVERIFY(menu->hasActiveFocus()); + QQuickOverlay *overlay = window->property("overlay").value<QQuickOverlay*>(); + QVERIFY(overlay); + QVERIFY(overlay->childItems().contains(menu->contentItem()->parentItem())); + QTRY_VERIFY(menu->isOpened()); + QVERIFY(!firstItem->hasActiveFocus()); + QVERIFY(!firstItem->property("highlighted").toBool()); + QCOMPARE(menu->currentIndex(), -1); + QCOMPARE(menu->contentItem()->property("currentIndex"), QVariant(-1)); + + QTest::keyClick(window, Qt::Key_Tab); + QVERIFY(firstItem->hasActiveFocus()); + QVERIFY(firstItem->hasVisualFocus()); + QVERIFY(firstItem->isHighlighted()); + QCOMPARE(firstItem->focusReason(), Qt::TabFocusReason); + QCOMPARE(menu->currentIndex(), 0); + QCOMPARE(menu->contentItem()->property("currentIndex"), QVariant(0)); + + QQuickMenuItem *secondItem = qobject_cast<QQuickMenuItem *>(menu->itemAt(1)); + QVERIFY(secondItem); + QTest::keyClick(window, Qt::Key_Tab); + QVERIFY(!firstItem->hasActiveFocus()); + QVERIFY(!firstItem->hasVisualFocus()); + QVERIFY(!firstItem->isHighlighted()); + QVERIFY(secondItem->hasActiveFocus()); + QVERIFY(secondItem->hasVisualFocus()); + QVERIFY(secondItem->isHighlighted()); + QCOMPARE(secondItem->focusReason(), Qt::TabFocusReason); + QCOMPARE(menu->currentIndex(), 1); + QCOMPARE(menu->contentItem()->property("currentIndex"), QVariant(1)); + + QSignalSpy secondTriggeredSpy(secondItem, SIGNAL(triggered())); + QTest::keyClick(window, Qt::Key_Space); + QCOMPARE(secondTriggeredSpy.size(), 1); + QTRY_COMPARE(visibleSpy.size(), 2); + QVERIFY(!menu->isVisible()); + QVERIFY(!overlay->childItems().contains(menu->contentItem())); + QVERIFY(!firstItem->hasActiveFocus()); + QVERIFY(!firstItem->hasVisualFocus()); + QVERIFY(!firstItem->isHighlighted()); + QVERIFY(!secondItem->hasActiveFocus()); + QVERIFY(!secondItem->hasVisualFocus()); + QVERIFY(!secondItem->isHighlighted()); + QCOMPARE(menu->currentIndex(), -1); + QCOMPARE(menu->contentItem()->property("currentIndex"), QVariant(-1)); + + // Enter/return should also work. + // Open the menu. + menu->open(); + QCOMPARE(visibleSpy.size(), 3); + QVERIFY(menu->isVisible()); + QTRY_VERIFY(menu->isOpened()); + // Give the first item focus. + QTest::keyClick(window, Qt::Key_Tab); + QVERIFY(firstItem->hasActiveFocus()); + QVERIFY(firstItem->hasVisualFocus()); + QVERIFY(firstItem->isHighlighted()); + QCOMPARE(firstItem->focusReason(), Qt::TabFocusReason); + QCOMPARE(menu->currentIndex(), 0); + QCOMPARE(menu->contentItem()->property("currentIndex"), QVariant(0)); + // Press enter. + QSignalSpy firstTriggeredSpy(firstItem, SIGNAL(triggered())); + QTest::keyClick(window, Qt::Key_Return); + QCOMPARE(firstTriggeredSpy.size(), 1); + QTRY_COMPARE(visibleSpy.size(), 4); + QVERIFY(!menu->isVisible()); + QVERIFY(!overlay->childItems().contains(menu->contentItem())); + QVERIFY(!firstItem->hasActiveFocus()); + QVERIFY(!firstItem->hasVisualFocus()); + QVERIFY(!firstItem->isHighlighted()); + QVERIFY(!secondItem->hasActiveFocus()); + QVERIFY(!secondItem->hasVisualFocus()); + QVERIFY(!secondItem->isHighlighted()); + QCOMPARE(menu->currentIndex(), -1); + QCOMPARE(menu->contentItem()->property("currentIndex"), QVariant(-1)); + + menu->open(); + QCOMPARE(visibleSpy.size(), 5); + QVERIFY(menu->isVisible()); + QVERIFY(overlay->childItems().contains(menu->contentItem()->parentItem())); + QTRY_VERIFY(menu->isOpened()); + QVERIFY(!firstItem->hasActiveFocus()); + QVERIFY(!firstItem->hasVisualFocus()); + QVERIFY(!firstItem->isHighlighted()); + QVERIFY(!secondItem->hasActiveFocus()); + QVERIFY(!secondItem->hasVisualFocus()); + QVERIFY(!secondItem->isHighlighted()); + QCOMPARE(menu->currentIndex(), -1); + QCOMPARE(menu->contentItem()->property("currentIndex"), QVariant(-1)); + + QTest::keyClick(window, Qt::Key_Down); + QVERIFY(firstItem->hasActiveFocus()); + QVERIFY(firstItem->hasVisualFocus()); + QVERIFY(firstItem->isHighlighted()); + QCOMPARE(firstItem->focusReason(), Qt::TabFocusReason); + + QTest::keyClick(window, Qt::Key_Down); + QVERIFY(secondItem->hasActiveFocus()); + QVERIFY(secondItem->hasVisualFocus()); + QVERIFY(secondItem->isHighlighted()); + QCOMPARE(secondItem->focusReason(), Qt::TabFocusReason); + + QTest::keyClick(window, Qt::Key_Down); + QQuickMenuItem *thirdItem = qobject_cast<QQuickMenuItem *>(menu->itemAt(2)); + QVERIFY(thirdItem); + QVERIFY(!firstItem->hasActiveFocus()); + QVERIFY(!firstItem->hasVisualFocus()); + QVERIFY(!firstItem->isHighlighted()); + QVERIFY(!secondItem->hasActiveFocus()); + QVERIFY(!secondItem->hasVisualFocus()); + QVERIFY(!secondItem->isHighlighted()); + QVERIFY(thirdItem->hasActiveFocus()); + QVERIFY(thirdItem->hasVisualFocus()); + QVERIFY(thirdItem->isHighlighted()); + QCOMPARE(thirdItem->focusReason(), Qt::TabFocusReason); + + // Key navigation shouldn't wrap by default. + QTest::keyClick(window, Qt::Key_Down); + QVERIFY(!firstItem->hasActiveFocus()); + QVERIFY(!firstItem->hasVisualFocus()); + QVERIFY(!firstItem->isHighlighted()); + QVERIFY(!secondItem->hasActiveFocus()); + QVERIFY(!secondItem->hasVisualFocus()); + QVERIFY(!secondItem->isHighlighted()); + QVERIFY(thirdItem->hasActiveFocus()); + QVERIFY(thirdItem->hasVisualFocus()); + QVERIFY(thirdItem->isHighlighted()); + QCOMPARE(thirdItem->focusReason(), Qt::TabFocusReason); + + QTest::keyClick(window, Qt::Key_Up); + QVERIFY(!firstItem->hasActiveFocus()); + QVERIFY(!firstItem->hasVisualFocus()); + QVERIFY(!firstItem->isHighlighted()); + QVERIFY(secondItem->hasActiveFocus()); + QVERIFY(secondItem->hasVisualFocus()); + QVERIFY(secondItem->isHighlighted()); + QCOMPARE(secondItem->focusReason(), Qt::BacktabFocusReason); + QVERIFY(!thirdItem->hasActiveFocus()); + QVERIFY(!thirdItem->hasVisualFocus()); + QVERIFY(!thirdItem->isHighlighted()); + + QTest::keyClick(window, Qt::Key_Backtab); + QVERIFY(firstItem->hasActiveFocus()); + QVERIFY(firstItem->hasVisualFocus()); + QVERIFY(firstItem->isHighlighted()); + QCOMPARE(firstItem->focusReason(), Qt::BacktabFocusReason); + QVERIFY(!secondItem->hasActiveFocus()); + QVERIFY(!secondItem->hasVisualFocus()); + QVERIFY(!secondItem->isHighlighted()); + QVERIFY(!thirdItem->hasActiveFocus()); + QVERIFY(!thirdItem->hasVisualFocus()); + QVERIFY(!thirdItem->isHighlighted()); + + QTest::keyClick(window, Qt::Key_Escape); + QTRY_COMPARE(visibleSpy.size(), 6); + QVERIFY(!menu->isVisible()); +} + +// QTBUG-70181 +void tst_QQuickMenu::disabledMenuItemKeyNavigation() +{ + if (!hasWindowActivation()) + QSKIP("Window activation is not supported"); + + if (QGuiApplication::styleHints()->tabFocusBehavior() != Qt::TabFocusAllControls) + QSKIP("This platform only allows tab focus for text controls"); + + QQuickControlsApplicationHelper helper(this, QLatin1String("disabledMenuItemKeyNavigation.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + + QQuickApplicationWindow *window = helper.appWindow; + centerOnScreen(window); + moveMouseAway(window); + window->show(); + window->requestActivate(); + QVERIFY(QTest::qWaitForWindowActive(window)); + QVERIFY(QGuiApplication::focusWindow() == window); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu*>(); + QCOMPARE(menu->currentIndex(), -1); + QCOMPARE(menu->contentItem()->property("currentIndex"), QVariant(-1)); + + QQuickMenuItem *firstItem = qobject_cast<QQuickMenuItem *>(menu->itemAt(0)); + QVERIFY(firstItem); + + QQuickMenuItem *secondItem = qobject_cast<QQuickMenuItem *>(menu->itemAt(1)); + QVERIFY(secondItem); + + QQuickMenuItem *thirdItem = qobject_cast<QQuickMenuItem *>(menu->itemAt(2)); + QVERIFY(thirdItem); + + menu->setFocus(true); + menu->open(); + QVERIFY(menu->isVisible()); + QTRY_VERIFY(menu->isOpened()); + QVERIFY(!firstItem->hasActiveFocus()); + QVERIFY(!firstItem->property("highlighted").toBool()); + QCOMPARE(menu->currentIndex(), -1); + + QTest::keyClick(window, Qt::Key_Tab); + QVERIFY(firstItem->hasActiveFocus()); + QVERIFY(firstItem->hasVisualFocus()); + QVERIFY(firstItem->isHighlighted()); + QCOMPARE(firstItem->focusReason(), Qt::TabFocusReason); + QCOMPARE(menu->currentIndex(), 0); + + // Shouldn't be possible to give focus to a disabled menu item. + QTest::keyClick(window, Qt::Key_Down); + QVERIFY(!secondItem->hasActiveFocus()); + QVERIFY(!secondItem->hasVisualFocus()); + QVERIFY(!secondItem->isHighlighted()); + QVERIFY(thirdItem->hasActiveFocus()); + QVERIFY(thirdItem->hasVisualFocus()); + QVERIFY(thirdItem->isHighlighted()); + QCOMPARE(thirdItem->focusReason(), Qt::TabFocusReason); + + QTest::keyClick(window, Qt::Key_Up); + QVERIFY(firstItem->hasActiveFocus()); + QVERIFY(firstItem->hasVisualFocus()); + QVERIFY(firstItem->isHighlighted()); + QCOMPARE(firstItem->focusReason(), Qt::BacktabFocusReason); + + QTest::keyClick(window, Qt::Key_Escape); + QTRY_VERIFY(!menu->isVisible()); +} + +void tst_QQuickMenu::mnemonics() +{ + if (!hasWindowActivation()) + QSKIP("Window activation is not supported"); + +#ifdef Q_OS_MACOS + QSKIP("Mnemonics are not used on macOS"); +#endif + + QQuickControlsApplicationHelper helper(this, QLatin1String("mnemonics.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + + QQuickWindow *window = helper.window; + window->show(); + window->requestActivate(); + QVERIFY(QTest::qWaitForWindowActive(window)); + + MnemonicKeySimulator keySim(window); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu *>(); + QQuickAction *action = window->property("action").value<QQuickAction *>(); + QQuickMenuItem *menuItem = window->property("menuItem").value<QQuickMenuItem *>(); + QQuickMenu *subMenu = window->property("subMenu").value<QQuickMenu *>(); + QQuickMenuItem *subMenuItem = window->property("subMenuItem").value<QQuickMenuItem *>(); + QVERIFY(menu && action && menuItem && subMenu && subMenuItem); + + keySim.press(Qt::Key_Alt); + menu->open(); + QTRY_VERIFY(menu->isOpened()); + + QSignalSpy actionSpy(action, &QQuickAction::triggered); + QVERIFY(actionSpy.isValid()); + keySim.click(Qt::Key_A); // "&Action" + QCOMPARE(actionSpy.size(), 1); + + menu->open(); + QTRY_VERIFY(menu->isOpened()); + + QSignalSpy menuItemSpy(menuItem, &QQuickMenuItem::triggered); + QVERIFY(menuItemSpy.isValid()); + keySim.click(Qt::Key_I); // "Menu &Item" + keySim.release(Qt::Key_Alt); + QCOMPARE(menuItemSpy.size(), 1); + + keySim.press(Qt::Key_Alt); + menu->open(); + QTRY_VERIFY(menu->isOpened()); + + keySim.click(Qt::Key_M); // "Sub &Menu" + QTRY_VERIFY(subMenu->isOpened()); + + QSignalSpy subMenuItemSpy(subMenuItem, &QQuickMenuItem::triggered); + QVERIFY(subMenuItemSpy.isValid()); + keySim.click(Qt::Key_S); // "&Sub Menu Item" + keySim.release(Qt::Key_Alt); + QCOMPARE(subMenuItemSpy.size(), 1); +} + +void tst_QQuickMenu::menuButton() +{ + if (!hasWindowActivation()) + QSKIP("Window activation is not supported"); + + if (QGuiApplication::styleHints()->tabFocusBehavior() != Qt::TabFocusAllControls) + QSKIP("This platform only allows tab focus for text controls"); + + QQuickControlsApplicationHelper helper(this, QLatin1String("applicationwindow.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + window->requestActivate(); + QVERIFY(QTest::qWaitForWindowActive(window)); + QVERIFY(QGuiApplication::focusWindow() == window); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu*>(); + QQuickButton *menuButton = window->property("menuButton").value<QQuickButton*>(); + QSignalSpy visibleSpy(menu, SIGNAL(visibleChanged())); + + menuButton->setVisible(true); + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, + menuButton->mapToScene(QPointF(menuButton->width() / 2, menuButton->height() / 2)).toPoint()); + QCOMPARE(visibleSpy.size(), 1); + QVERIFY(menu->isVisible()); + QTRY_VERIFY(menu->isOpened()); + + QTest::keyClick(window, Qt::Key_Tab); + QQuickItem *firstItem = menu->itemAt(0); + QVERIFY(firstItem->hasActiveFocus()); +} + +void tst_QQuickMenu::addItem() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("addItem.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu*>(); + QVERIFY(menu); + menu->open(); + QVERIFY(menu->isVisible()); + + QQuickItem *menuItem = menu->itemAt(0); + QVERIFY(menuItem); + QTRY_VERIFY(!QQuickItemPrivate::get(menuItem)->culled); // QTBUG-53262 + + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, + menuItem->mapToScene(QPointF(menuItem->width() / 2, menuItem->height() / 2)).toPoint()); + QTRY_VERIFY(!menu->isVisible()); +} + +void tst_QQuickMenu::menuSeparator() +{ + if (!hasWindowActivation()) + QSKIP("Window activation is not supported"); + + QQuickControlsApplicationHelper helper(this, QLatin1String("menuSeparator.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickWindow *window = helper.window; + centerOnScreen(window); + moveMouseAway(window); + window->show(); + QVERIFY(QTest::qWaitForWindowActive(window)); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu*>(); + QVERIFY(menu); + menu->open(); + QVERIFY(menu->isVisible()); + + QQuickMenuItem *newMenuItem = qobject_cast<QQuickMenuItem*>(menu->itemAt(0)); + QVERIFY(newMenuItem); + QCOMPARE(newMenuItem->text(), QStringLiteral("New")); + + QQuickMenuSeparator *menuSeparator = qobject_cast<QQuickMenuSeparator*>(menu->itemAt(1)); + QVERIFY(menuSeparator); + + QQuickMenuItem *saveMenuItem = qobject_cast<QQuickMenuItem*>(menu->itemAt(2)); + QVERIFY(saveMenuItem); + QCOMPARE(saveMenuItem->text(), QStringLiteral("Save")); + QTRY_VERIFY(!QQuickItemPrivate::get(saveMenuItem)->culled); // QTBUG-53262 + QTRY_VERIFY(menu->isOpened()); + + // Clicking on items should still close the menu. + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, + newMenuItem->mapToScene(QPointF(newMenuItem->width() / 2, newMenuItem->height() / 2)).toPoint()); + QTRY_VERIFY(!menu->isVisible()); + + menu->open(); + QTRY_VERIFY(menu->isOpened()); + + // Clicking on a separator shouldn't close the menu. + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, + menuSeparator->mapToScene(QPointF(menuSeparator->width() / 2, menuSeparator->height() / 2)).toPoint()); + QVERIFY(menu->isVisible()); + + // Clicking on items should still close the menu. + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, + saveMenuItem->mapToScene(QPointF(saveMenuItem->width() / 2, saveMenuItem->height() / 2)).toPoint()); + QTRY_VERIFY(!menu->isVisible()); + + moveMouseAway(window); + + menu->open(); + QVERIFY(menu->isVisible()); + QTRY_VERIFY(menu->isOpened()); + + // Key navigation skips separators + QTest::keyClick(window, Qt::Key_Down); + QVERIFY(newMenuItem->hasActiveFocus()); + QVERIFY(newMenuItem->hasVisualFocus()); + QCOMPARE(newMenuItem->focusReason(), Qt::TabFocusReason); + + QTest::keyClick(window, Qt::Key_Down); + QVERIFY(saveMenuItem->hasActiveFocus()); + QVERIFY(saveMenuItem->hasVisualFocus()); + QCOMPARE(saveMenuItem->focusReason(), Qt::TabFocusReason); + + QTest::keyClick(window, Qt::Key_Down); + QVERIFY(saveMenuItem->hasActiveFocus()); + QVERIFY(saveMenuItem->hasVisualFocus()); + QCOMPARE(saveMenuItem->focusReason(), Qt::TabFocusReason); + + QTest::keyClick(window, Qt::Key_Up); + QVERIFY(newMenuItem->hasActiveFocus()); + QVERIFY(newMenuItem->hasVisualFocus()); + QCOMPARE(newMenuItem->focusReason(), Qt::BacktabFocusReason); + + QTest::keyClick(window, Qt::Key_Up); + QVERIFY(newMenuItem->hasActiveFocus()); + QVERIFY(newMenuItem->hasVisualFocus()); + QCOMPARE(newMenuItem->focusReason(), Qt::BacktabFocusReason); +} + +void tst_QQuickMenu::repeater() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("repeater.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickWindow *window = helper.window; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu*>(); + QVERIFY(menu); + menu->open(); + QVERIFY(menu->isVisible()); + + QObject *repeater = window->property("repeater").value<QObject*>(); + QVERIFY(repeater); + + int count = repeater->property("count").toInt(); + QCOMPARE(count, 5); + + for (int i = 0; i < count; ++i) { + QQuickItem *item = menu->itemAt(i); + QVERIFY(item); + QCOMPARE(item->property("idx").toInt(), i); + + QQuickItem *repeaterItem = nullptr; + QVERIFY(QMetaObject::invokeMethod(repeater, "itemAt", Q_RETURN_ARG(QQuickItem*, repeaterItem), Q_ARG(int, i))); + QCOMPARE(item, repeaterItem); + } + + repeater->setProperty("model", 3); + + count = repeater->property("count").toInt(); + QCOMPARE(count, 3); + + for (int i = 0; i < count; ++i) { + QQuickItem *item = menu->itemAt(i); + QVERIFY(item); + QCOMPARE(item->property("idx").toInt(), i); + + QQuickItem *repeaterItem = nullptr; + QVERIFY(QMetaObject::invokeMethod(repeater, "itemAt", Q_RETURN_ARG(QQuickItem*, repeaterItem), Q_ARG(int, i))); + QCOMPARE(item, repeaterItem); + } +} + +void tst_QQuickMenu::order() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("order.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickWindow *window = helper.window; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu*>(); + QVERIFY(menu); + menu->open(); + QVERIFY(menu->isVisible()); + + const QStringList texts = {"dynamic_0", "static_1", "repeated_2", "repeated_3", "static_4", "dynamic_5", "dynamic_6"}; + + for (int i = 0; i < texts.size(); ++i) { + QQuickItem *item = menu->itemAt(i); + QVERIFY(item); + QCOMPARE(item->property("text").toString(), texts.at(i)); + } +} + +#if QT_CONFIG(cursor) +void tst_QQuickMenu::popup() +{ +#if defined(Q_OS_ANDROID) + QSKIP("Setting cursor position is not supported on Android"); +#endif + if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) + QSKIP("Setting cursor position is not supported on Wayland"); + + // Try moving the cursor from the current position + // Skip if it fails since the test relies on moving the cursor + const QPoint point = QCursor::pos() + QPoint(1, 1); + QCursor::setPos(point); + if (!QTest::qWaitFor([point]{ return QCursor::pos() == point; })) + QSKIP("Setting cursor position is not supported on this platform"); + + QQuickControlsApplicationHelper helper(this, QLatin1String("popup.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + centerOnScreen(window); + moveMouseAway(window); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu *>(); + QVERIFY(menu); + + QQuickMenuItem *menuItem1 = window->property("menuItem1").value<QQuickMenuItem *>(); + QVERIFY(menuItem1); + + QQuickMenuItem *menuItem2 = window->property("menuItem2").value<QQuickMenuItem *>(); + QVERIFY(menuItem2); + + QQuickMenuItem *menuItem3 = window->property("menuItem3").value<QQuickMenuItem *>(); + QVERIFY(menuItem3); + + QQuickItem *button = window->property("button").value<QQuickItem *>(); + QVERIFY(button); + + QPoint oldCursorPos = QCursor::pos(); + QPoint cursorPos = window->mapToGlobal(QPoint(11, 22)); + QCursor::setPos(cursorPos); + QTRY_COMPARE(QCursor::pos(), cursorPos); + + QVERIFY(QMetaObject::invokeMethod(window, "popupAtCursor")); + QCOMPARE(menu->parentItem(), window->contentItem()); + QCOMPARE(menu->currentIndex(), -1); + QCOMPARE(menu->contentItem()->property("currentIndex").toInt(), -1); + const qreal elevenOrLeftMargin = qMax(qreal(11), menu->leftMargin()); + const qreal twentyTwoOrTopMargin = qMax(qreal(22), menu->topMargin()); + // If the Menu has large margins, it may be moved to stay within them. + // QTBUG-75503: QTRY_COMPARE doesn't use qFuzzyCompare() in all cases, + // meaning a lot of these comparisons could trigger a 10 second wait; + // use QTRY_VERIFY and qFuzzyCompare instead. + QTRY_VERIFY(qFuzzyCompare(menu->x(), elevenOrLeftMargin)); + QTRY_VERIFY(qFuzzyCompare(menu->y(), twentyTwoOrTopMargin)); + menu->close(); + + QVERIFY(QMetaObject::invokeMethod(window, "popupAtPos", Q_ARG(QVariant, QPointF(33, 44)))); + QCOMPARE(menu->parentItem(), window->contentItem()); + QCOMPARE(menu->currentIndex(), -1); + QCOMPARE(menu->contentItem()->property("currentIndex").toInt(), -1); + QTRY_VERIFY(qFuzzyCompare(menu->x(), qMax(qreal(33), menu->leftMargin()))); + QTRY_VERIFY(qFuzzyCompare(menu->y(), qMax(qreal(44), menu->topMargin()))); + menu->close(); + + QVERIFY(QMetaObject::invokeMethod(window, "popupAtCoord", Q_ARG(QVariant, 55), Q_ARG(QVariant, 66))); + QCOMPARE(menu->parentItem(), window->contentItem()); + QCOMPARE(menu->currentIndex(), -1); + QCOMPARE(menu->contentItem()->property("currentIndex").toInt(), -1); + QTRY_VERIFY(qFuzzyCompare(menu->x(), qMax(qreal(55), menu->leftMargin()))); + QTRY_VERIFY(qFuzzyCompare(menu->y(), qMax(qreal(66), menu->topMargin()))); + menu->close(); + + menu->setParentItem(nullptr); + QVERIFY(QMetaObject::invokeMethod(window, "popupAtParentCursor", Q_ARG(QVariant, QVariant::fromValue(button)))); + QCOMPARE(menu->parentItem(), button); + QCOMPARE(menu->currentIndex(), -1); + QCOMPARE(menu->contentItem()->property("currentIndex").toInt(), -1); + QTRY_VERIFY(qFuzzyCompare(menu->x(), button->mapFromScene(QPointF(elevenOrLeftMargin, twentyTwoOrTopMargin)).x())); + QTRY_VERIFY(qFuzzyCompare(menu->y(), button->mapFromScene(QPointF(elevenOrLeftMargin, twentyTwoOrTopMargin)).y())); + menu->close(); + + menu->setParentItem(nullptr); + QVERIFY(QMetaObject::invokeMethod(window, "popupAtParentPos", Q_ARG(QVariant, QVariant::fromValue(button)), Q_ARG(QVariant, QPointF(-11, -22)))); + QCOMPARE(menu->parentItem(), button); + QCOMPARE(menu->currentIndex(), -1); + QCOMPARE(menu->contentItem()->property("currentIndex").toInt(), -1); + // Don't need to worry about margins here because we're opening close + // to the center of the window. + QTRY_VERIFY(qFuzzyCompare(menu->x(), -11)); + QTRY_VERIFY(qFuzzyCompare(menu->y(), -22)); + QCOMPARE(menu->popupItem()->position(), button->mapToScene(QPointF(-11, -22))); + menu->close(); + + menu->setParentItem(nullptr); + QVERIFY(QMetaObject::invokeMethod(window, "popupAtParentCoord", Q_ARG(QVariant, QVariant::fromValue(button)), Q_ARG(QVariant, -33), Q_ARG(QVariant, -44))); + QCOMPARE(menu->parentItem(), button); + QCOMPARE(menu->currentIndex(), -1); + QCOMPARE(menu->contentItem()->property("currentIndex").toInt(), -1); + QTRY_VERIFY(qFuzzyCompare(menu->x(), -33)); + QTRY_VERIFY(qFuzzyCompare(menu->y(), -44)); + QCOMPARE(menu->popupItem()->position(), button->mapToScene(QPointF(-33, -44))); + menu->close(); + + const qreal twelveOrLeftMargin = qMax(qreal(12), menu->leftMargin()); + cursorPos = window->mapToGlobal(QPoint(twelveOrLeftMargin, window->height() / 2)); + QCursor::setPos(cursorPos); + QTRY_COMPARE(QCursor::pos(), cursorPos); + + const QList<QQuickMenuItem *> menuItems = QList<QQuickMenuItem *>() << menuItem1 << menuItem2 << menuItem3; + for (QQuickMenuItem *menuItem : menuItems) { + menu->resetParentItem(); + + QVERIFY(QMetaObject::invokeMethod(window, "popupItemAtCursor", Q_ARG(QVariant, QVariant::fromValue(menuItem)))); + QCOMPARE(menu->currentIndex(), menuItems.indexOf(menuItem)); + QCOMPARE(menu->contentItem()->property("currentIndex").toInt(), menuItems.indexOf(menuItem)); + QTRY_VERIFY(qFuzzyCompare(menu->x(), twelveOrLeftMargin)); + QTRY_VERIFY(qFuzzyCompare(menu->y(), window->height() / 2 - menu->topPadding() - menuItem->y())); + menu->close(); + + QVERIFY(QMetaObject::invokeMethod(window, "popupItemAtPos", Q_ARG(QVariant, QPointF(33, window->height() / 3)), Q_ARG(QVariant, QVariant::fromValue(menuItem)))); + QCOMPARE(menu->currentIndex(), menuItems.indexOf(menuItem)); + QCOMPARE(menu->contentItem()->property("currentIndex").toInt(), menuItems.indexOf(menuItem)); + QTRY_VERIFY(qFuzzyCompare(menu->x(), 33)); + QTRY_VERIFY(qFuzzyCompare(menu->y(), window->height() / 3 - menu->topPadding() - menuItem->y())); + menu->close(); + + QVERIFY(QMetaObject::invokeMethod(window, "popupItemAtCoord", Q_ARG(QVariant, 55), Q_ARG(QVariant, window->height() / 3 * 2), Q_ARG(QVariant, QVariant::fromValue(menuItem)))); + QCOMPARE(menu->currentIndex(), menuItems.indexOf(menuItem)); + QCOMPARE(menu->contentItem()->property("currentIndex").toInt(), menuItems.indexOf(menuItem)); + QTRY_VERIFY(qFuzzyCompare(menu->x(), 55)); + QTRY_COMPARE_WITH_TIMEOUT(menu->y(), window->height() / 3 * 2 - menu->topPadding() - menuItem->y(), 500); + menu->close(); + + menu->setParentItem(nullptr); + QVERIFY(QMetaObject::invokeMethod(window, "popupItemAtParentCursor", Q_ARG(QVariant, QVariant::fromValue(button)), Q_ARG(QVariant, QVariant::fromValue(menuItem)))); + QCOMPARE(menu->parentItem(), button); + QCOMPARE(menu->currentIndex(), menuItems.indexOf(menuItem)); + QCOMPARE(menu->contentItem()->property("currentIndex").toInt(), menuItems.indexOf(menuItem)); + QTRY_VERIFY(qFuzzyCompare(menu->x(), button->mapFromScene(QPoint(twelveOrLeftMargin, window->height() / 2)).x())); + QTRY_VERIFY(qFuzzyCompare(menu->y(), button->mapFromScene(QPoint(twelveOrLeftMargin, window->height() / 2)).y() - menu->topPadding() - menuItem->y())); + menu->close(); + + menu->setParentItem(nullptr); + QVERIFY(QMetaObject::invokeMethod(window, "popupItemAtParentPos", Q_ARG(QVariant, QVariant::fromValue(button)), Q_ARG(QVariant, QPointF(-11, -22)), Q_ARG(QVariant, QVariant::fromValue(menuItem)))); + QCOMPARE(menu->parentItem(), button); + QCOMPARE(menu->currentIndex(), menuItems.indexOf(menuItem)); + QCOMPARE(menu->contentItem()->property("currentIndex").toInt(), menuItems.indexOf(menuItem)); + QTRY_VERIFY(qFuzzyCompare(menu->x(), -11)); + QTRY_VERIFY(qFuzzyCompare(menu->y(), -22 - menu->topPadding() - menuItem->y())); + QCOMPARE(menu->popupItem()->position(), button->mapToScene(QPointF(-11, -22 - menu->topPadding() - menuItem->y()))); + menu->close(); + + menu->setParentItem(nullptr); + QVERIFY(QMetaObject::invokeMethod(window, "popupItemAtParentCoord", Q_ARG(QVariant, QVariant::fromValue(button)), Q_ARG(QVariant, -33), Q_ARG(QVariant, -44), Q_ARG(QVariant, QVariant::fromValue(menuItem)))); + QCOMPARE(menu->parentItem(), button); + QCOMPARE(menu->currentIndex(), menuItems.indexOf(menuItem)); + QCOMPARE(menu->contentItem()->property("currentIndex").toInt(), menuItems.indexOf(menuItem)); + QTRY_VERIFY(qFuzzyCompare(menu->x(), -33)); + QTRY_VERIFY(qFuzzyCompare(menu->y(), -44 - menu->topPadding() - menuItem->y())); + QCOMPARE(menu->popupItem()->position(), button->mapToScene(QPointF(-33, -44 - menu->topPadding() - menuItem->y()))); + menu->close(); + } + + QCursor::setPos(oldCursorPos); + QTRY_COMPARE(QCursor::pos(), oldCursorPos); +} +#endif // QT_CONFIG(cursor) + +void tst_QQuickMenu::actions() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("actions.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickWindow *window = helper.window; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu *>(); + QVERIFY(menu); + + QPointer<QQuickAction> action1 = menu->actionAt(0); + QVERIFY(!action1.isNull()); + + QPointer<QQuickAction> action3 = menu->actionAt(2); + QVERIFY(!action3.isNull()); + + QVERIFY(!menu->actionAt(1)); + QVERIFY(!menu->actionAt(3)); + + QPointer<QQuickMenuItem> menuItem1 = qobject_cast<QQuickMenuItem *>(menu->itemAt(0)); + QVERIFY(!menuItem1.isNull()); + QCOMPARE(menuItem1->action(), action1.data()); + QCOMPARE(menuItem1->text(), "action1"); + + QPointer<QQuickMenuItem> menuItem2 = qobject_cast<QQuickMenuItem *>(menu->itemAt(1)); + QVERIFY(!menuItem2.isNull()); + QVERIFY(!menuItem2->action()); + QCOMPARE(menuItem2->text(), "menuitem2"); + + QPointer<QQuickMenuItem> menuItem3 = qobject_cast<QQuickMenuItem *>(menu->itemAt(2)); + QVERIFY(!menuItem3.isNull()); + QCOMPARE(menuItem3->action(), action3.data()); + QCOMPARE(menuItem3->text(), "action3"); + + QPointer<QQuickMenuItem> menuItem4 = qobject_cast<QQuickMenuItem *>(menu->itemAt(3)); + QVERIFY(!menuItem4.isNull()); + QVERIFY(!menuItem4->action()); + QCOMPARE(menuItem4->text(), "menuitem4"); + + // takeAction(int) does not destroy the action, but does destroy the respective item + QCOMPARE(menu->takeAction(0), action1.data()); + QVERIFY(!menu->itemAt(3)); + QCoreApplication::sendPostedEvents(action1, QEvent::DeferredDelete); + QVERIFY(!action1.isNull()); + QCoreApplication::sendPostedEvents(menuItem1, QEvent::DeferredDelete); + QVERIFY(menuItem1.isNull()); + + // takeAction(int) does not destroy an item that doesn't have an action + QVERIFY(!menuItem2->subMenu()); + QVERIFY(!menu->takeAction(0)); + QCoreApplication::sendPostedEvents(menuItem2, QEvent::DeferredDelete); + QVERIFY(!menuItem2.isNull()); + + // addAction(Action) re-creates the respective item in the menu + menu->addAction(action1); + menuItem1 = qobject_cast<QQuickMenuItem *>(menu->itemAt(3)); + QVERIFY(!menuItem1.isNull()); + QCOMPARE(menuItem1->action(), action1.data()); + + // removeAction(Action) destroys both the action and the respective item + menu->removeAction(action1); + QVERIFY(!menu->itemAt(3)); + QCoreApplication::sendPostedEvents(action1, QEvent::DeferredDelete); + QVERIFY(action1.isNull()); + QCoreApplication::sendPostedEvents(menuItem1, QEvent::DeferredDelete); + QVERIFY(menuItem1.isNull()); +} + +#if QT_CONFIG(shortcut) +void tst_QQuickMenu::actionShortcuts() +{ + if (!hasWindowActivation()) + QSKIP("Window activation is not supported"); + + QQuickControlsApplicationHelper helper(this, QLatin1String("actionShortcuts.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickWindow *window = helper.window; + window->show(); + QVERIFY(QTest::qWaitForWindowActive(window)); + + // Try the menu's shortcut. + QQuickMenu *menu = window->property("menu").value<QQuickMenu *>(); + QVERIFY(menu); + QPointer<QQuickAction> action1 = menu->actionAt(0); + QVERIFY(action1); + QCOMPARE(action1->shortcut(), QKeySequence(Qt::Key_A)); + + QSignalSpy action1TriggeredSpy(action1, SIGNAL(triggered())); + QVERIFY(action1TriggeredSpy.isValid()); + + QTest::keyClick(window, Qt::Key_A); + QCOMPARE(action1TriggeredSpy.size(), 1); + + // Try the sub-menu. + QQuickMenu *subMenu = window->property("subMenu").value<QQuickMenu *>(); + QVERIFY(subMenu); + QPointer<QQuickAction> subMenuAction1 = subMenu->actionAt(0); + QVERIFY(subMenuAction1); + QCOMPARE(subMenuAction1->shortcut(), QKeySequence(Qt::Key_B)); + + QSignalSpy subMenuAction1TriggeredSpy(subMenuAction1, SIGNAL(triggered())); + QVERIFY(subMenuAction1TriggeredSpy.isValid()); + + QTest::keyClick(window, Qt::Key_B); + QCOMPARE(subMenuAction1TriggeredSpy.size(), 1); + + // Try the button menu. + QQuickMenu *buttonMenu = window->property("buttonMenu").value<QQuickMenu *>(); + QVERIFY(buttonMenu); + QPointer<QQuickAction> buttonMenuAction1 = buttonMenu->actionAt(0); + QVERIFY(buttonMenuAction1); + QCOMPARE(buttonMenuAction1->shortcut(), QKeySequence(Qt::Key_C)); + + QSignalSpy buttonMenuAction1TriggeredSpy(buttonMenuAction1, SIGNAL(triggered())); + QVERIFY(buttonMenuAction1TriggeredSpy.isValid()); + + QTest::keyClick(window, Qt::Key_C); + QCOMPARE(buttonMenuAction1TriggeredSpy.size(), 1); +} +#endif + +void tst_QQuickMenu::removeTakeItem() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("removeTakeItem.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickWindow *window = helper.window; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu *>(); + QVERIFY(menu); + + QPointer<QQuickMenuItem> menuItem1 = window->property("menuItem1").value<QQuickMenuItem *>(); + QVERIFY(!menuItem1.isNull()); + QCOMPARE(menuItem1->menu(), menu); + + QPointer<QQuickMenuItem> menuItem2 = window->property("menuItem2").value<QQuickMenuItem *>(); + QVERIFY(!menuItem2.isNull()); + QCOMPARE(menuItem2->menu(), menu); + + QPointer<QQuickMenuItem> menuItem3 = window->property("menuItem3").value<QQuickMenuItem *>(); + QVERIFY(!menuItem3.isNull()); + QCOMPARE(menuItem3->menu(), menu); + + // takeItem(int) does not destroy + QVariant ret; + QVERIFY(QMetaObject::invokeMethod(window, "takeSecondItem", Q_RETURN_ARG(QVariant, ret))); + QCOMPARE(ret.value<QQuickMenuItem *>(), menuItem2); + QVERIFY(!menuItem2->menu()); + QCoreApplication::sendPostedEvents(menuItem2, QEvent::DeferredDelete); + QVERIFY(!menuItem2.isNull()); + + // removeItem(Item) destroys + QVERIFY(QMetaObject::invokeMethod(window, "removeFirstItem")); + QVERIFY(!menuItem1->menu()); + QCoreApplication::sendPostedEvents(menuItem1, QEvent::DeferredDelete); + QVERIFY(menuItem1.isNull()); + + // removeItem(null) must not call removeItem(0) + QVERIFY(QMetaObject::invokeMethod(window, "removeNullItem")); + QCOMPARE(menuItem3->menu(), menu); + QCoreApplication::sendPostedEvents(menuItem3, QEvent::DeferredDelete); + QVERIFY(!menuItem3.isNull()); +} + +void tst_QQuickMenu::subMenuMouse_data() +{ + QTest::addColumn<bool>("cascade"); + + QTest::newRow("cascading") << true; + QTest::newRow("non-cascading") << false; +} + +void tst_QQuickMenu::subMenuMouse() +{ + if ((QGuiApplication::platformName() == QLatin1String("offscreen")) + || (QGuiApplication::platformName() == QLatin1String("minimal"))) + QSKIP("Mouse hovering not functional on offscreen/minimal platforms"); + + QFETCH(bool, cascade); + + QQuickControlsApplicationHelper helper(this, QLatin1String("subMenus.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + centerOnScreen(window); + moveMouseAway(window); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(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); + subMenu1->setCascade(cascade); + QCOMPARE(subMenu1->cascade(), cascade); + + QQuickMenu *subMenu2 = window->property("subMenu2").value<QQuickMenu *>(); + QVERIFY(subMenu2); + subMenu2->setCascade(cascade); + QCOMPARE(subMenu2->cascade(), cascade); + + QQuickMenu *subSubMenu1 = window->property("subSubMenu1").value<QQuickMenu *>(); + QVERIFY(subSubMenu1); + subSubMenu1->setCascade(cascade); + QCOMPARE(subSubMenu1->cascade(), cascade); + + mainMenu->open(); + QVERIFY(mainMenu->isVisible()); + QTRY_VERIFY(mainMenu->isOpened()); + 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()); + QTRY_COMPARE(mainMenu->isVisible(), cascade); + QVERIFY(subMenu1->isVisible()); + QTRY_VERIFY(subMenu1->isOpened()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + + // on Android mouse hover will not open and close sub-menus +#ifndef Q_OS_ANDROID + // 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()); + QVERIFY(!subSubMenu1->isVisible()); + if (cascade) { + QTRY_VERIFY(subSubMenu1->isVisible()); + QTRY_VERIFY(subSubMenu1->isOpened()); + } + + // 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()); + QTRY_VERIFY(!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()); + if (!cascade) { + QVERIFY(!subSubMenu1->isVisible()); + } else { + QTRY_VERIFY(subSubMenu1->isVisible()); + QTRY_VERIFY(subSubMenu1->isOpened()); + } + + // 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); + QTRY_COMPARE(subMenu1->isVisible(), !cascade); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); +#else + QQuickMenuItem *mainMenuItem1 = qobject_cast<QQuickMenuItem *>(mainMenu->itemAt(0)); + QVERIFY(mainMenuItem1); +#endif // !Q_OS_ANDROID + + // close all menus by click triggering an item + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, mainMenuItem1->mapToScene(QPoint(1, 1)).toPoint()); + QTRY_VERIFY(!mainMenu->isVisible()); + QTRY_VERIFY(!subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); +} + +void tst_QQuickMenu::subMenuDisabledMouse_data() +{ + subMenuMouse_data(); +} + +// QTBUG-69540 +void tst_QQuickMenu::subMenuDisabledMouse() +{ + if ((QGuiApplication::platformName() == QLatin1String("offscreen")) + || (QGuiApplication::platformName() == QLatin1String("minimal"))) + QSKIP("Mouse hovering not functional on offscreen/minimal platforms"); + + QFETCH(bool, cascade); + + QQuickControlsApplicationHelper helper(this, QLatin1String("subMenuDisabled.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + centerOnScreen(window); + moveMouseAway(window); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *mainMenu = window->property("mainMenu").value<QQuickMenu *>(); + QVERIFY(mainMenu); + mainMenu->setCascade(cascade); + QCOMPARE(mainMenu->cascade(), cascade); + + QQuickMenuItem *menuItem1 = qobject_cast<QQuickMenuItem *>(mainMenu->itemAt(0)); + QVERIFY(menuItem1); + + QQuickMenu *subMenu = window->property("subMenu").value<QQuickMenu *>(); + QVERIFY(subMenu); + + mainMenu->open(); + QTRY_VERIFY(mainMenu->isOpened()); + QVERIFY(!menuItem1->isHighlighted()); + QVERIFY(!subMenu->isVisible()); + + // Hover-highlighting does not work on Android +#ifndef Q_OS_ANDROID + // Generate a hover event to set the current index + QTest::mouseMove(window, menuItem1->mapToScene(QPoint(2, 2)).toPoint()); + QTRY_VERIFY(menuItem1->isHighlighted()); +#endif + // Open the sub-menu with a mouse click. + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, menuItem1->mapToScene(QPoint(1, 1)).toPoint()); + // Need to use the TRY variant here, + // as e.g. Material, iOS style menus have transitions and don't open/close immediately. + QTRY_COMPARE(mainMenu->isVisible(), cascade); + QTRY_VERIFY(subMenu->isOpened()); + QTRY_VERIFY(menuItem1->isHighlighted()); + // Now the sub-menu is open. The current behavior is that the first menu item + // in the new menu is highlighted; make sure that we choose the next item if + // the first is disabled. + QQuickMenuItem *subMenuItem1 = qobject_cast<QQuickMenuItem *>(subMenu->itemAt(0)); + QVERIFY(subMenuItem1); + QQuickMenuItem *subMenuItem2 = qobject_cast<QQuickMenuItem *>(subMenu->itemAt(1)); + QVERIFY(subMenuItem2); + QVERIFY(!subMenuItem1->isHighlighted()); + QVERIFY(subMenuItem2->isHighlighted()); + + // Close all menus by clicking on the item that isn't disabled. + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, subMenuItem2->mapToScene(QPoint(1, 1)).toPoint()); + QTRY_VERIFY(!mainMenu->isVisible()); + QTRY_VERIFY(!subMenu->isVisible()); +} + +void tst_QQuickMenu::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_QQuickMenu::subMenuKeyboard() +{ + if (!hasWindowActivation()) + QSKIP("Window activation is not supported"); + + QFETCH(bool, cascade); + QFETCH(bool, mirrored); + + QQuickControlsApplicationHelper helper(this, QLatin1String("subMenus.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + centerOnScreen(window); + moveMouseAway(window); + window->show(); + QVERIFY(QTest::qWaitForWindowActive(window)); + + if (mirrored) { + QQmlExpression mirroringExpression(qmlContext(window), window, + "LayoutMirroring.childrenInherit = true; LayoutMirroring.enabled = true"); + QVERIFY2(mirroringExpression.evaluate().isValid(), qPrintable(mirroringExpression.error().toString())); + } + + 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()); + QTRY_VERIFY(mainMenu->isOpened()); + QVERIFY(!subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + + // navigate to the sub-menu item and trigger it to open the sub-menu + 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); + QTRY_COMPARE(mainMenu->isVisible(), cascade); + QVERIFY(subMenu1->isVisible()); + QTRY_VERIFY(subMenu1->isOpened()); + 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()); + QTRY_COMPARE(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); + QTRY_COMPARE(subMenu1->isVisible(), cascade); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(subSubMenu1->isVisible()); + QTRY_VERIFY(subSubMenu1->isOpened()); + + // 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()); + QTRY_VERIFY(!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); + QTRY_VERIFY(!subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + QTest::keyClick(window, Qt::Key_Escape); + QTRY_VERIFY(!mainMenu->isVisible()); + QVERIFY(!subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); +} + +void tst_QQuickMenu::subMenuDisabledKeyboard_data() +{ + subMenuKeyboard_data(); +} + +// QTBUG-69540 +void tst_QQuickMenu::subMenuDisabledKeyboard() +{ + if (!hasWindowActivation()) + QSKIP("Window activation is not supported"); + + QFETCH(bool, cascade); + QFETCH(bool, mirrored); + + QQuickControlsApplicationHelper helper(this, QLatin1String("subMenuDisabled.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + centerOnScreen(window); + moveMouseAway(window); + window->show(); + QVERIFY(QTest::qWaitForWindowActive(window)); + + if (mirrored) { + QQmlExpression mirroringExpression(qmlContext(window), window, + "LayoutMirroring.childrenInherit = true; LayoutMirroring.enabled = true"); + QVERIFY2(mirroringExpression.evaluate().isValid(), qPrintable(mirroringExpression.error().toString())); + } + + QQuickMenu *mainMenu = window->property("mainMenu").value<QQuickMenu *>(); + QVERIFY(mainMenu); + mainMenu->setCascade(cascade); + QCOMPARE(mainMenu->cascade(), cascade); + + QQuickMenuItem *menuItem1 = qobject_cast<QQuickMenuItem *>(mainMenu->itemAt(0)); + QVERIFY(menuItem1); + + QQuickMenu *subMenu = window->property("subMenu").value<QQuickMenu *>(); + QVERIFY(subMenu); + + mainMenu->open(); + QVERIFY(mainMenu->isVisible()); + QTRY_VERIFY(mainMenu->isOpened()); + QVERIFY(!menuItem1->isHighlighted()); + QVERIFY(!subMenu->isVisible()); + + // Highlight the top-level menu item. + QTest::keyClick(window, Qt::Key_Down); + QVERIFY(menuItem1->isHighlighted()); + + QQuickMenuItem *subMenuItem1 = qobject_cast<QQuickMenuItem *>(subMenu->itemAt(0)); + QVERIFY(subMenuItem1); + QQuickMenuItem *subMenuItem2 = qobject_cast<QQuickMenuItem *>(subMenu->itemAt(1)); + QVERIFY(subMenuItem2); + + // Open the sub-menu. + QTest::keyClick(window, mirrored ? Qt::Key_Left : Qt::Key_Right); + // The first sub-menu item is disabled, so it should highlight the second one. + QVERIFY(!subMenuItem1->isHighlighted()); + QVERIFY(subMenuItem2->isHighlighted()); + + // Close the menus with escape. + QTest::keyClick(window, Qt::Key_Escape); + QTRY_COMPARE(mainMenu->isVisible(), cascade); + QTRY_VERIFY(!subMenu->isVisible()); + QTest::keyClick(window, Qt::Key_Escape); + QTRY_VERIFY(!mainMenu->isVisible()); + QVERIFY(!subMenu->isVisible()); +} + +/* + QCOMPARE() compares doubles with 1-in-1e12 precision, which is too fine for these tests. + Casting to floats, compared with 1-in-1e5 precision, gives more robust results. +*/ +#define FLOAT_EQ(u, v) QCOMPARE(float(u), float(v)) + +void tst_QQuickMenu::subMenuPosition_data() +{ + QTest::addColumn<bool>("cascade"); + QTest::addColumn<bool>("flip"); + QTest::addColumn<bool>("mirrored"); + QTest::addColumn<qreal>("overlap"); + + QTest::newRow("cascading") << true << false << false << 0.0; + QTest::newRow("cascading,flip") << true << true << false << 0.0; + QTest::newRow("cascading,overlap") << true << false << false << 10.0; + QTest::newRow("cascading,flip,overlap") << true << true << false << 10.0; + QTest::newRow("cascading,mirrored") << true << false << true << 0.0; + QTest::newRow("cascading,mirrored,flip") << true << true << true << 0.0; + QTest::newRow("cascading,mirrored,overlap") << true << false << true << 10.0; + QTest::newRow("cascading,mirrored,flip,overlap") << true << true << true << 10.0; + QTest::newRow("non-cascading") << false << false << false << 0.0; +} + +void tst_QQuickMenu::subMenuPosition() +{ + QFETCH(bool, cascade); + QFETCH(bool, flip); + QFETCH(bool, mirrored); + QFETCH(qreal, overlap); + + QQuickControlsApplicationHelper helper(this, QLatin1String("subMenus.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + + // Ensure that the default size of the window fits three menus side by side. + QQuickMenu *mainMenu = window->property("mainMenu").value<QQuickMenu *>(); + QVERIFY(mainMenu); + window->setWidth(mainMenu->width() * 3 + mainMenu->leftMargin() + mainMenu->rightMargin()); + + // the default size of the window fits three menus side by side. + // when testing flipping, we resize the window so that the first + // sub-menu fits, but the second doesn't + if (flip) + window->setWidth(window->width() - mainMenu->width()); + + centerOnScreen(window); + moveMouseAway(window); +#ifndef Q_OS_ANDROID + window->show(); +#else + // On Android the desired size does not fit into the screen, so we just + // call showNormal. This will make the window larger than the screen, but + // all the geometry calculations will be correct. Otherwise we'll get + // unpredictable results + window->showNormal(); +#endif + QVERIFY(QTest::qWaitForWindowExposed(window)); + + if (mirrored) { + QQmlExpression mirroringExpression(qmlContext(window), window, + "LayoutMirroring.childrenInherit = true; LayoutMirroring.enabled = true"); + QVERIFY2(mirroringExpression.evaluate().isValid(), qPrintable(mirroringExpression.error().toString())); + } + + 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); + + // choose the main menu position so that there's room for the + // sub-menus to cascade to the left when mirrored + if (mirrored) + mainMenu->setX(window->width() - mainMenu->width()); + + mainMenu->open(); + QVERIFY(mainMenu->isVisible()); + QTRY_VERIFY(mainMenu->isOpened()); + QVERIFY(!subMenu1->isVisible()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + + // open the sub-menu (never flips) + QQuickMenuItem *subMenu1Item = qobject_cast<QQuickMenuItem *>(mainMenu->itemAt(1)); + QVERIFY(subMenu1Item); + QCOMPARE(subMenu1Item->subMenu(), subMenu1); + emit subMenu1Item->triggered(); + QTRY_COMPARE(mainMenu->isVisible(), cascade); + QVERIFY(subMenu1->isVisible()); + QTRY_VERIFY(subMenu1->isOpened()); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(!subSubMenu1->isVisible()); + + if (cascade) { + QCOMPARE(subMenu1->parentItem(), subMenu1Item); + // vertically aligned to the parent menu item + // We cast to float here because we want to use its larger tolerance for equality (because it has less precision than double). + FLOAT_EQ(subMenu1->popupItem()->y(), mainMenu->popupItem()->y() + subMenu1Item->y()); + if (mirrored) { + // on the left of the parent menu + FLOAT_EQ(subMenu1->popupItem()->x(), mainMenu->popupItem()->x() - subMenu1->width() + overlap); + } else { + // on the right of the parent menu + FLOAT_EQ(subMenu1->popupItem()->x(), mainMenu->popupItem()->x() + mainMenu->width() - overlap); + } + } else { + QCOMPARE(subMenu1->parentItem(), mainMenu->parentItem()); + // centered over the parent menu + FLOAT_EQ(subMenu1->popupItem()->x(), mainMenu->popupItem()->x() + (mainMenu->width() - subMenu1->width()) / 2); + FLOAT_EQ(subMenu1->popupItem()->y(), mainMenu->popupItem()->y() + (mainMenu->height() - subMenu1->height()) / 2); + } + + // open the sub-sub-menu (can flip) + QQuickMenuItem *subSubMenu1Item = qobject_cast<QQuickMenuItem *>(subMenu1->itemAt(2)); + QVERIFY(subSubMenu1Item); + QCOMPARE(subSubMenu1Item->subMenu(), subSubMenu1); + emit subSubMenu1Item->triggered(); + QTRY_COMPARE(mainMenu->isVisible(), cascade); + QTRY_COMPARE(subMenu1->isVisible(), cascade); + QVERIFY(!subMenu2->isVisible()); + QVERIFY(subSubMenu1->isVisible()); + QTRY_VERIFY(subSubMenu1->isOpened()); + + if (cascade) { + QCOMPARE(subSubMenu1->parentItem(), subSubMenu1Item); + // vertically aligned to the parent menu item + FLOAT_EQ(subSubMenu1->popupItem()->y(), subMenu1->popupItem()->y() + subSubMenu1Item->y()); + if (mirrored != flip) { + // on the left of the parent menu + FLOAT_EQ(subSubMenu1->popupItem()->x(), subMenu1->popupItem()->x() - subSubMenu1->width() + overlap); + } else { + // on the right of the parent menu + FLOAT_EQ(subSubMenu1->popupItem()->x(), subMenu1->popupItem()->x() + subMenu1->width() - overlap); + } + } else { + QCOMPARE(subSubMenu1->parentItem(), subMenu1->parentItem()); + // centered over the parent menu + FLOAT_EQ(subSubMenu1->popupItem()->x(), subMenu1->popupItem()->x() + (subMenu1->width() - subSubMenu1->width()) / 2); + FLOAT_EQ(subSubMenu1->popupItem()->y(), subMenu1->popupItem()->y() + (subMenu1->height() - subSubMenu1->height()) / 2); + } +} + +#undef FLOAT_EQ + +void tst_QQuickMenu::subMenuWithIcon() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("subMenus.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + + QQuickMenu *mainMenu = window->property("mainMenu").value<QQuickMenu *>(); + QVERIFY(mainMenu); + + QQuickMenu *subMenu = window->property("subMenu1").value<QQuickMenu *>(); + QVERIFY(subMenu); + + const int iconWidth = 14; + const int iconHeight = 20; + const QUrl iconSource("qrc:/qt-project.org/imports/QtQuick/Controls/Basic/images/check.png"); + + QQuickIcon icon; + icon.setSource(iconSource); + icon.setWidth(iconWidth); + icon.setHeight(iconHeight); + + subMenu->setIcon(icon); + QCOMPARE(subMenu->icon().source(), iconSource); + QCOMPARE(subMenu->icon().width(), iconWidth); + QCOMPARE(subMenu->icon().height(), iconHeight); + + QQuickMenuItem *subMenuItem = qobject_cast<QQuickMenuItem *>(mainMenu->itemAt(1)); + QVERIFY(subMenuItem); + QCOMPARE(subMenuItem->icon().source(), iconSource); + QCOMPARE(subMenuItem->icon().width(), iconWidth); + QCOMPARE(subMenuItem->icon().height(), iconHeight); +} + +void tst_QQuickMenu::addRemoveSubMenus() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("subMenus.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickWindow *window = helper.window; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *mainMenu = window->property("mainMenu").value<QQuickMenu *>(); + QVERIFY(mainMenu); + + QVERIFY(!mainMenu->menuAt(0)); + + QPointer<QQuickMenu> subMenu1 = window->property("subMenu1").value<QQuickMenu *>(); + QVERIFY(!subMenu1.isNull()); + QCOMPARE(mainMenu->menuAt(1), subMenu1.data()); + + QVERIFY(!mainMenu->menuAt(2)); + + QPointer<QQuickMenu> subMenu2 = window->property("subMenu2").value<QQuickMenu *>(); + QVERIFY(!subMenu2.isNull()); + QCOMPARE(mainMenu->menuAt(3), subMenu2.data()); + + QVERIFY(!mainMenu->menuAt(4)); + + QPointer<QQuickMenu> subSubMenu1 = window->property("subSubMenu1").value<QQuickMenu *>(); + QVERIFY(!subSubMenu1.isNull()); + + // takeMenu(int) does not destroy the menu, but does destroy the respective item in the parent menu + QPointer<QQuickMenuItem> subSubMenu1Item = qobject_cast<QQuickMenuItem *>(subMenu1->itemAt(2)); + QVERIFY(subSubMenu1Item); + QCOMPARE(subSubMenu1Item->subMenu(), subSubMenu1.data()); + QCOMPARE(subMenu1->takeMenu(2), subSubMenu1.data()); + QVERIFY(!subMenu1->itemAt(2)); + QCoreApplication::sendPostedEvents(subSubMenu1, QEvent::DeferredDelete); + QVERIFY(!subSubMenu1.isNull()); + QCoreApplication::sendPostedEvents(subSubMenu1Item, QEvent::DeferredDelete); + QVERIFY(subSubMenu1Item.isNull()); + + // takeMenu(int) does not destroy an item that doesn't present a menu + QPointer<QQuickMenuItem> subMenuItem1 = qobject_cast<QQuickMenuItem *>(subMenu1->itemAt(0)); + QVERIFY(subMenuItem1); + QVERIFY(!subMenuItem1->subMenu()); + QVERIFY(!subMenu1->takeMenu(0)); + QCoreApplication::sendPostedEvents(subMenuItem1, QEvent::DeferredDelete); + QVERIFY(!subMenuItem1.isNull()); + + // addMenu(Menu) re-creates the respective item in the parent menu + subMenu1->addMenu(subSubMenu1); + subSubMenu1Item = qobject_cast<QQuickMenuItem *>(subMenu1->itemAt(2)); + QVERIFY(!subSubMenu1Item.isNull()); + + // removeMenu(Menu) destroys both the menu and the respective item in the parent menu + subMenu1->removeMenu(subSubMenu1); + QVERIFY(!subMenu1->itemAt(2)); + QCoreApplication::sendPostedEvents(subSubMenu1, QEvent::DeferredDelete); + QVERIFY(subSubMenu1.isNull()); + QCoreApplication::sendPostedEvents(subSubMenu1Item, QEvent::DeferredDelete); + QVERIFY(subSubMenu1Item.isNull()); +} + +void tst_QQuickMenu::scrollable_data() +{ + QTest::addColumn<QString>("qmlFilePath"); + + QTest::addRow("Window") << QString::fromLatin1("windowScrollable.qml"); + QTest::addRow("ApplicationWindow") << QString::fromLatin1("applicationWindowScrollable.qml"); + QTest::addRow("WithPadding") << QString::fromLatin1("scrollableWithPadding.qml"); +} + +void tst_QQuickMenu::scrollable() +{ + QFETCH(QString, qmlFilePath); + + QQuickControlsApplicationHelper helper(this, qmlFilePath); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickWindow *window = helper.window; +#ifndef Q_OS_ANDROID + window->show(); +#else + window->showNormal(); +#endif + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu*>(); + menu->open(); + QVERIFY(menu->isVisible()); + + QQuickItem *contentItem = menu->contentItem(); + QCOMPARE(contentItem->property("interactive").toBool(), true); +} + +void tst_QQuickMenu::disableWhenTriggered_data() +{ + QTest::addColumn<int>("menuItemIndex"); + QTest::addColumn<int>("subMenuItemIndex"); + + QTest::addRow("Action") << 0 << -1; + QTest::addRow("MenuItem with Action") << 1 << -1; + QTest::addRow("MenuItem with Action declared outside menu") << 2 << -1; + QTest::addRow("MenuItem with no Action") << 3 << -1; + + QTest::addRow("Sub-Action") << 4 << 0; + QTest::addRow("Sub-MenuItem with Action declared inside") << 4 << 1; + QTest::addRow("Sub-MenuItem with Action declared outside menu") << 4 << 2; + QTest::addRow("Sub-MenuItem with no Action") << 4 << 3; +} + +// Tests that the menu is dismissed when a menu item sets "enabled = false" in onTriggered(). +void tst_QQuickMenu::disableWhenTriggered() +{ + if ((QGuiApplication::platformName() == QLatin1String("offscreen")) + || (QGuiApplication::platformName() == QLatin1String("minimal"))) + QSKIP("Mouse hovering not functional on offscreen/minimal platforms"); + + QFETCH(int, menuItemIndex); + QFETCH(int, subMenuItemIndex); + + QQuickControlsApplicationHelper helper(this, QLatin1String("disableWhenTriggered.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickWindow *window = helper.window; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *menu = window->findChild<QQuickMenu*>("Menu"); + QVERIFY(menu); + + menu->open(); + QVERIFY(menu->isVisible()); + QTRY_VERIFY(menu->isOpened()); + + QPointer<QQuickMenuItem> menuItem = qobject_cast<QQuickMenuItem*>(menu->itemAt(menuItemIndex)); + QVERIFY(menuItem); + + if (subMenuItemIndex == -1) { + // Click a top-level menu item. + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, + menuItem->mapToScene(QPointF(menuItem->width() / 2, menuItem->height() / 2)).toPoint()); + QCOMPARE(menuItem->isEnabled(), false); + QTRY_VERIFY(!menu->isVisible()); + } else { + // Click a sub-menu item. + QPointer<QQuickMenu> subMenu = menuItem->subMenu(); + QVERIFY(subMenu); + + QPointer<QQuickMenuItem> subMenuItem = qobject_cast<QQuickMenuItem*>(subMenu->itemAt(subMenuItemIndex)); + QVERIFY(subMenuItem); + + // First, open the sub-menu. +#if !defined(Q_OS_ANDROID) and !defined(Q_OS_WEBOS) + QTest::mouseMove(window, menuItem->mapToScene(QPoint(1, 1)).toPoint()); +#else + // On Android and webOS mouseHover does not open sub-menu, so just click on it + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, + menuItem->mapToScene(QPointF(menuItem->width() / 2, menuItem->height() / 2)).toPoint()); +#endif + + QTRY_VERIFY(subMenu->isVisible()); +#ifndef Q_OS_ANDROID + QVERIFY(menuItem->isHovered()); + QTRY_VERIFY(subMenu->contentItem()->property("contentHeight").toReal() > 0.0); +#endif + + // Click the item. + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, + subMenuItem->mapToScene(QPointF(subMenuItem->width() / 2, subMenuItem->height() / 2)).toPoint()); + QCOMPARE(subMenuItem->isEnabled(), false); + QTRY_VERIFY(!menu->isVisible()); + } +} + +void tst_QQuickMenu::menuItemWidth_data() +{ + QTest::addColumn<bool>("mirrored"); + + QTest::newRow("non-mirrored") << false; + QTest::newRow("mirrored") << true; +} + +void tst_QQuickMenu::menuItemWidth() +{ + QFETCH(bool, mirrored); + + QQuickControlsApplicationHelper helper(this, QLatin1String("menuItemWidths.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + if (mirrored) { + QQmlExpression mirroringExpression(qmlContext(window), window, + "LayoutMirroring.childrenInherit = true; LayoutMirroring.enabled = true"); + QVERIFY2(mirroringExpression.evaluate().isValid(), qPrintable(mirroringExpression.error().toString())); + } + + QQuickMenu *menu = window->property("menu").value<QQuickMenu *>(); + QVERIFY(menu); + menu->open(); + QTRY_VERIFY(menu->isOpened()); + for (int i = 0; i < menu->count(); ++i) + QCOMPARE(menu->itemAt(i)->width(), menu->availableWidth()); +} + +void tst_QQuickMenu::menuItemWidthAfterMenuWidthChanged_data() +{ + QTest::addColumn<bool>("mirrored"); + + QTest::newRow("non-mirrored") << false; + QTest::newRow("mirrored") << true; +} + +void tst_QQuickMenu::menuItemWidthAfterMenuWidthChanged() +{ + QFETCH(bool, mirrored); + + QQuickControlsApplicationHelper helper(this, QLatin1String("menuItemWidths.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + if (mirrored) { + QQmlExpression mirroringExpression(qmlContext(window), window, + "LayoutMirroring.childrenInherit = true; LayoutMirroring.enabled = true"); + QVERIFY2(mirroringExpression.evaluate().isValid(), qPrintable(mirroringExpression.error().toString())); + } + + QQuickMenu *menu = window->property("menu").value<QQuickMenu *>(); + QVERIFY(menu); + menu->open(); + QTRY_VERIFY(menu->isOpened()); + for (int i = 0; i < menu->count(); ++i) { + // Check that the width of menu items is correct before we resize the menu. + const QQuickItem *item = menu->itemAt(i); + QVERIFY2(qFuzzyCompare(item->width(), menu->availableWidth()), + qPrintable(QString::fromLatin1("Expected width of %1 to be %2, but it's %3") + .arg(item->objectName()).arg(menu->availableWidth()).arg(item->width()))); + } + + menu->setWidth(menu->width() + 10); + + // Check that the width of menu items is correct after we resize the menu. + for (int i = 0; i < menu->count(); ++i) { + // Check that the width of menu items is correct after we resize the menu. + const QQuickItem *item = menu->itemAt(i); + QVERIFY2(qFuzzyCompare(item->width(), menu->availableWidth()), + qPrintable(QString::fromLatin1("Expected width of %1 to be %2, but it's %3") + .arg(item->objectName()).arg(menu->availableWidth()).arg(item->width()))); + } +} + +void tst_QQuickMenu::menuItemWidthAfterImplicitWidthChanged_data() +{ + QTest::addColumn<bool>("mirrored"); + + QTest::newRow("non-mirrored") << false; + QTest::newRow("mirrored") << true; +} + +void tst_QQuickMenu::menuItemWidthAfterImplicitWidthChanged() +{ + QFETCH(bool, mirrored); + + QQuickControlsApplicationHelper helper(this, QLatin1String("menuItemWidths.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + if (mirrored) { + QQmlExpression mirroringExpression(qmlContext(window), window, + "LayoutMirroring.childrenInherit = true; LayoutMirroring.enabled = true"); + QVERIFY2(mirroringExpression.evaluate().isValid(), qPrintable(mirroringExpression.error().toString())); + } + + QQuickMenu *menu = window->property("menu").value<QQuickMenu *>(); + QVERIFY(menu); + menu->open(); + QTRY_VERIFY(menu->isOpened()); + // Check that the width of the menu item is correct before we change its font size. + QQuickMenuItem *menuItem = qobject_cast<QQuickMenuItem*>(menu->itemAt(0)); + QCOMPARE(menuItem->width(), menu->availableWidth()); + + // Add some text to increase the implicitWidth of the MenuItem. + const qreal oldImplicitWidth = menuItem->implicitWidth(); + for (int i = 0; menuItem->implicitWidth() <= oldImplicitWidth; ++i) { + menuItem->setText(menuItem->text() + QLatin1String("---")); + if (i == 100) + QFAIL("Shouldn't need 100 iterations to increase MenuItem's implicitWidth; something is wrong here"); + } + + // Check that the width of the menu item is correct after we change its font size. + QCOMPARE(menuItem->width(), menu->availableWidth()); +} + +void tst_QQuickMenu::menuItemWidthAfterRetranslate() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("menuItemWidths.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu *>(); + QVERIFY(menu); + menu->open(); + QTRY_VERIFY(menu->isOpened()); + for (int i = 0; i < menu->count(); ++i) { + // Check that the width of each menu item is correct before we retranslate. + const QQuickItem *item = menu->itemAt(i); + QVERIFY2(qFuzzyCompare(item->width(), menu->availableWidth()), + qPrintable(QString::fromLatin1("Expected width of %1 to be %2, but it's %3") + .arg(item->objectName()).arg(menu->availableWidth()).arg(item->width()))); + } + + // Call retranslate() and cause all bindings to be re-evaluated. + helper.engine.retranslate(); + + for (int i = 0; i < menu->count(); ++i) { + // Check that the width of each menu item is correct after we retranslate. + const QQuickItem *item = menu->itemAt(i); + QVERIFY2(qFuzzyCompare(item->width(), menu->availableWidth()), + qPrintable(QString::fromLatin1("Expected width of %1 to be %2, but it's %3") + .arg(item->objectName()).arg(menu->availableWidth()).arg(item->width()))); + } +} + +void tst_QQuickMenu::giveMenuItemFocusOnButtonPress() +{ + if (!hasWindowActivation()) + QSKIP("Window activation is not supported"); + + QQuickControlsApplicationHelper helper(this, QLatin1String("giveMenuItemFocusOnButtonPress.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + QVERIFY(QTest::qWaitForWindowActive(window)); + + // Press enter on the button to open the menu. + QQuickButton *menuButton = window->property("menuButton").value<QQuickButton*>(); + QVERIFY(menuButton); + menuButton->forceActiveFocus(); + QVERIFY(menuButton->hasActiveFocus()); + + QSignalSpy clickedSpy(window, SIGNAL(menuButtonClicked())); + QVERIFY(clickedSpy.isValid()); + + QTest::keyClick(window, Qt::Key_Return); + QCOMPARE(clickedSpy.size(), 1); + + // The menu should still be open. + QQuickMenu *menu = window->property("menu").value<QQuickMenu*>(); + QVERIFY(menu); + QTRY_VERIFY(menu->isOpened()); +} + +void tst_QQuickMenu::customMenuCullItems() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("customMenuCullItems.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu*>(); + QVERIFY(menu); + menu->open(); + QTRY_VERIFY(menu->isOpened()); + + QQuickItem *menuItemFirst = menu->itemAt(0); + QQuickItem *menuItemLast = menu->itemAt(menu->count() - 1); + QVERIFY(menuItemFirst); + QVERIFY(menuItemLast); + QTRY_VERIFY(!QQuickItemPrivate::get(menuItemFirst)->culled); + QTRY_VERIFY(QQuickItemPrivate::get(menuItemLast)->culled); +} + +void tst_QQuickMenu::customMenuUseRepeaterAsTheContentItem() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("customMenuUseRepeaterAsTheContentItem.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickMenu *menu = window->property("menu").value<QQuickMenu*>(); + QVERIFY(menu); + menu->open(); + QTRY_VERIFY(menu->isVisible()); + + QQuickItem *menuItemFirst = menu->itemAt(0); + QQuickItem *menuItemLast = menu->itemAt(menu->count() - 1); + QTRY_VERIFY(!QQuickItemPrivate::get(menuItemFirst)->culled); + QTRY_VERIFY(!QQuickItemPrivate::get(menuItemLast)->culled); +} + +QTEST_QUICKCONTROLS_MAIN(tst_QQuickMenu) + +#include "tst_qquickmenu.moc" |