qt6-bb10/qtdeclarative/tests/auto/quickcontrols/qquickmenubar/tst_qquickmenubar.cpp

1872 lines
76 KiB
C++

// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtGui/qpa/qplatformintegration.h>
#include <QtGui/qpa/qplatformtheme.h>
#include <QtGui/private/qguiapplication_p.h>
#include <QtTest>
#include <QtQml>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#include <QtQuickTestUtils/private/visualtestutils_p.h>
#include <QtQuickTemplates2/private/qquickapplicationwindow_p.h>
#include <QtQuickTemplates2/private/qquickbutton_p.h>
#include <QtQuickTemplates2/private/qquickmenu_p.h>
#include <QtQuickTemplates2/private/qquickmenu_p_p.h>
#include <QtQuickTemplates2/private/qquickmenubar_p.h>
#include <QtQuickTemplates2/private/qquickmenubar_p_p.h>
#include <QtQuickTemplates2/private/qquickmenubaritem_p.h>
#include <QtQuickTemplates2/private/qquickmenuitem_p.h>
#include <QtQuickTemplates2/private/qquickpopupwindow_p_p.h>
#include <QtQuickControlsTestUtils/private/controlstestutils_p.h>
#include <QtQuickControlsTestUtils/private/qtest_quickcontrols_p.h>
using namespace QQuickVisualTestUtils;
using namespace QQuickControlsTestUtils;
class tst_qquickmenubar : public QQmlDataTest
{
Q_OBJECT
public:
tst_qquickmenubar();
private slots:
void init() override;
void cleanup();
void delegate();
void mouse_data();
void mouse();
void touch();
void keys_data();
void keys();
void mnemonics_data();
void mnemonics();
void altNavigation();
void addRemove_data();
void addRemove();
void addRemoveInlineMenus_data();
void addRemoveInlineMenus();
void addRemoveMenuFromQml_data();
void addRemoveMenuFromQml();
void insert_data();
void insert();
void showAndHideMenuBarItems_data();
void showAndHideMenuBarItems();
void removeMenuThatIsOpen();
void addRemoveExistingMenus_data();
void addRemoveExistingMenus();
void checkHighlightWhenMenuDismissed_data();
void checkHighlightWhenMenuDismissed();
void hoverAfterClosingWithEscape_data();
void hoverAfterClosingWithEscape();
void closeByClickingOutside_data();
void closeByClickingOutside();
void AA_DontUseNativeMenuBar();
void AA_DontUseNativeMenuWindows();
void containerItems_data();
void containerItems();
void mixedContainerItems_data();
void mixedContainerItems();
void applicationWindow_data();
void applicationWindow();
void menubarAsHeader_data();
void menubarAsHeader();
void menuPosition_data();
void menuPosition();
void changeDelegate_data();
void changeDelegate();
void invalidDelegate_data();
void invalidDelegate();
void panMenuBar_data();
void panMenuBar();
private:
bool nativeMenuBarSupported = false;
bool popupWindowsSupported = false;
QScopedPointer<QPointingDevice> touchScreen = QScopedPointer<QPointingDevice>(QTest::createTouchDevice());
};
QPoint itemSceneCenter(const QQuickItem *item)
{
return item->mapToScene(QPointF(item->width() / 2, item->height() / 2)).toPoint();
}
tst_qquickmenubar::tst_qquickmenubar()
: QQmlDataTest(QT_QMLTEST_DATADIR)
{
qputenv("QML_NO_TOUCH_COMPRESSION", "1");
QQuickMenuBar mb;
nativeMenuBarSupported = QQuickMenuBarPrivate::get(&mb)->useNativeMenuBar();
#if defined(Q_OS_WINDOWS) || defined(Q_OS_MACOS)
popupWindowsSupported = QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::Capability::MultipleWindows);
#endif
}
void tst_qquickmenubar::init()
{
// Enable native menubars and windows by default.
// Note that some tests will set this property to 'true', which
// is why we need to set it back to 'false' here.
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, false);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuWindows, false);
}
void tst_qquickmenubar::cleanup()
{
// For some reason, it's not impossible for popups to already exist, when a test is executed.
if (QGuiApplicationPrivate::popupCount() > 0)
QGuiApplicationPrivate::closeAllPopups();
QTRY_COMPARE(QGuiApplicationPrivate::popupCount(), 0);
}
void tst_qquickmenubar::delegate()
{
QQmlApplicationEngine engine(testFileUrl("empty.qml"));
QScopedPointer<QQuickMenuBar> menuBar(qobject_cast<QQuickMenuBar *>(engine.rootObjects().value(0)));
QVERIFY(menuBar);
QQmlComponent *delegate = menuBar->delegate();
QVERIFY(delegate);
QScopedPointer<QQuickMenuBarItem> item(qobject_cast<QQuickMenuBarItem *>(delegate->create()));
QVERIFY(item);
}
void tst_qquickmenubar::mouse_data()
{
QTest::addColumn<QQuickPopup::PopupType>("popupType");
QTest::newRow("Popup.Item") << QQuickPopup::Item;
// Uncomment when popup windows work 100% (QTBUG-128479)
// if (popupWindowsSupported)
// QTest::newRow("Popup.Window") << QQuickPopup::Window;
}
void tst_qquickmenubar::mouse()
{
SKIP_IF_NO_WINDOW_ACTIVATION;
SKIP_IF_NO_MOUSE_HOVER;
QFETCH(QQuickPopup::PopupType, popupType);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuWindows);
QQmlApplicationEngine engine(testFileUrl("menubaritems.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
centerOnScreen(window.data());
moveMouseAway(window.data());
QVERIFY(QTest::qWaitForWindowActive(window.data()));
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
QQuickMenu *fileMenuBarMenu = menuBar->menuAt(0);
QQuickMenu *editMenuBarMenu = menuBar->menuAt(1);
QQuickMenu *viewMenuBarMenu = menuBar->menuAt(2);
QQuickMenu *helpMenuBarMenu = menuBar->menuAt(3);
QVERIFY(fileMenuBarMenu && editMenuBarMenu && viewMenuBarMenu && helpMenuBarMenu);
fileMenuBarMenu->setPopupType(popupType);
editMenuBarMenu->setPopupType(popupType);
viewMenuBarMenu->setPopupType(popupType);
helpMenuBarMenu->setPopupType(popupType);
QQuickMenuBarItem *fileMenuBarItem = qobject_cast<QQuickMenuBarItem *>(fileMenuBarMenu->parentItem());
QQuickMenuBarItem *editMenuBarItem = qobject_cast<QQuickMenuBarItem *>(editMenuBarMenu->parentItem());
QQuickMenuBarItem *viewMenuBarItem = qobject_cast<QQuickMenuBarItem *>(viewMenuBarMenu->parentItem());
QQuickMenuBarItem *helpMenuBarItem = qobject_cast<QQuickMenuBarItem *>(helpMenuBarMenu->parentItem());
QVERIFY(fileMenuBarItem && editMenuBarItem && viewMenuBarItem && helpMenuBarItem);
// highlight a menubar item
QTest::mouseMove(window.data(), itemSceneCenter(fileMenuBarItem));
#ifndef Q_OS_ANDROID
// Android theme does not use hover effects, so moving the mouse would not
// highlight an item
QVERIFY(fileMenuBarItem->isHighlighted());
#endif
QVERIFY(!fileMenuBarMenu->isVisible());
// highlight another menubar item
QTest::mouseMove(window.data(), itemSceneCenter(editMenuBarItem));
#ifndef Q_OS_ANDROID
// Android theme does not use hover effects, so moving the mouse would not
// highlight an item
QVERIFY(!fileMenuBarItem->isHighlighted());
QVERIFY(editMenuBarItem->isHighlighted());
#endif
QVERIFY(!fileMenuBarMenu->isVisible());
QVERIFY(!editMenuBarMenu->isVisible());
// trigger a menubar item to open a menu - it should open on press
QTest::mousePress(window.data(), Qt::LeftButton, Qt::NoModifier, itemSceneCenter(editMenuBarItem));
QVERIFY(editMenuBarItem->isHighlighted());
QVERIFY(editMenuBarMenu->isVisible());
QTRY_VERIFY(editMenuBarMenu->isOpened());
QTest::mouseRelease(window.data(), Qt::LeftButton, Qt::NoModifier, itemSceneCenter(editMenuBarItem));
// re-trigger a menubar item to hide the menu - it should close on press
QTest::mousePress(window.data(), Qt::LeftButton, Qt::NoModifier, itemSceneCenter(editMenuBarItem));
QVERIFY(editMenuBarItem->isHighlighted());
QVERIFY(editMenuBarItem->hasActiveFocus());
QTRY_VERIFY(!editMenuBarMenu->isVisible());
QTest::mouseRelease(window.data(), Qt::LeftButton, Qt::NoModifier, itemSceneCenter(editMenuBarItem));
// re-trigger a menubar item to show the menu again
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier, itemSceneCenter(editMenuBarItem));
QVERIFY(editMenuBarItem->isHighlighted());
QVERIFY(editMenuBarMenu->isVisible());
QTRY_VERIFY(editMenuBarMenu->isOpened());
// highlight another menubar item to open another menu
QTest::mouseMove(window.data(), itemSceneCenter(helpMenuBarItem));
#ifdef Q_OS_ANDROID
// Android theme does not use hover effects, so moving the mouse would not
// highlight an item. Add a mouse click to change menubar item selection.
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier,
helpMenuBarItem->mapToScene(QPointF(helpMenuBarItem->width() / 2,
helpMenuBarItem->height() / 2)).toPoint());
#endif
QVERIFY(!fileMenuBarItem->isHighlighted());
QVERIFY(!editMenuBarItem->isHighlighted());
QVERIFY(!viewMenuBarItem->isHighlighted());
QVERIFY(helpMenuBarItem->isHighlighted());
QVERIFY(!fileMenuBarMenu->isVisible());
QVERIFY(!viewMenuBarMenu->isVisible());
QVERIFY(helpMenuBarMenu->isVisible());
QTRY_VERIFY(!editMenuBarMenu->isVisible());
QTRY_VERIFY(helpMenuBarMenu->isOpened());
// trigger a menu item to close the menu
QQuickMenuItem *aboutMenuItem = qobject_cast<QQuickMenuItem *>(helpMenuBarMenu->itemAt(0));
QVERIFY(aboutMenuItem);
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier, aboutMenuItem->mapToScene(QPointF(aboutMenuItem->width() / 2, aboutMenuItem->height() / 2)).toPoint());
QVERIFY(!helpMenuBarItem->isHighlighted());
QTRY_VERIFY(!helpMenuBarMenu->isVisible());
// highlight a menubar item
QTest::mouseMove(window.data(), itemSceneCenter(editMenuBarItem));
#ifndef Q_OS_ANDROID
// Android theme does not use hover effects, so moving the mouse would not
// highlight an item
QVERIFY(editMenuBarItem->isHighlighted());
QVERIFY(!helpMenuBarItem->isHighlighted());
#endif
QVERIFY(!editMenuBarMenu->isVisible());
QVERIFY(!helpMenuBarMenu->isVisible());
// trigger a menubar item to open a menu
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier, itemSceneCenter(viewMenuBarItem));
QVERIFY(!editMenuBarItem->isHighlighted());
QVERIFY(viewMenuBarItem->isHighlighted());
QVERIFY(viewMenuBarMenu->isVisible());
QTRY_VERIFY(viewMenuBarMenu->isOpened());
// trigger a menu item to open a sub-menu
QQuickMenuItem *alignmentSubMenuItem = qobject_cast<QQuickMenuItem *>(viewMenuBarMenu->itemAt(0));
QVERIFY(alignmentSubMenuItem);
QQuickMenu *alignmentSubMenu = alignmentSubMenuItem->subMenu();
QVERIFY(alignmentSubMenu);
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier, itemSceneCenter(alignmentSubMenuItem));
#if !defined(Q_OS_ANDROID) and !defined(Q_OS_WEBOS)
// The screen on Android is too small to fit the whole hierarchy, so the
// Alignment sub-menu is shown on top of View menu.
// WebOS also shows alignment sub-menu on top of View menu.
QVERIFY(viewMenuBarMenu->isVisible());
#endif
QVERIFY(alignmentSubMenu->isVisible());
QTRY_VERIFY(alignmentSubMenu->isOpened());
// trigger a menu item to open a sub-sub-menu
QQuickMenuItem *verticalSubMenuItem = qobject_cast<QQuickMenuItem *>(alignmentSubMenu->itemAt(1));
QVERIFY(verticalSubMenuItem);
QQuickMenu *verticalSubMenu = verticalSubMenuItem->subMenu();
QVERIFY(verticalSubMenu);
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier, itemSceneCenter(verticalSubMenuItem));
#if !defined(Q_OS_ANDROID) and !defined(Q_OS_WEBOS)
// The screen on Android is too small to fit the whole hierarchy, so the
// Vertical sub-menu is shown on top of View menu and Alignment sub-menu.
// WebOS also shows vertical sub-menu on top of View menu and Alignment sub-menu.
QVERIFY(viewMenuBarMenu->isVisible());
QVERIFY(alignmentSubMenu->isVisible());
#endif
QVERIFY(verticalSubMenu->isVisible());
QTRY_VERIFY(verticalSubMenu->isOpened());
// trigger a menu item to close the whole chain of menus
QQuickMenuItem *centerMenuItem = qobject_cast<QQuickMenuItem *>(verticalSubMenu->itemAt(1));
QVERIFY(centerMenuItem);
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier, itemSceneCenter(centerMenuItem));
QVERIFY(!viewMenuBarItem->isHighlighted());
QTRY_VERIFY(!viewMenuBarMenu->isVisible());
QTRY_VERIFY(!alignmentSubMenu->isVisible());
QTRY_VERIFY(!verticalSubMenu->isVisible());
// re-highlight the same menubar item
#ifndef Q_OS_ANDROID
// Android theme does not use hover effects, so moving the mouse would not
// highlight an item
QTest::mouseMove(window.data(), viewMenuBarItem->mapToScene(QPointF(viewMenuBarItem->width() / 2, viewMenuBarItem->height() / 2)).toPoint());
QVERIFY(viewMenuBarItem->isHighlighted());
#endif
// re-open the chain of menus
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier, itemSceneCenter(viewMenuBarItem));
QTRY_VERIFY(viewMenuBarMenu->isOpened());
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier, itemSceneCenter(alignmentSubMenuItem));
QTRY_VERIFY(alignmentSubMenu->isOpened());
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier, itemSceneCenter(verticalSubMenuItem));
QTRY_VERIFY(verticalSubMenu->isOpened());
// click outside to close the whole chain of menus
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier, QPoint(window->width() - 1, window->height() - 1));
QVERIFY(!viewMenuBarItem->isHighlighted());
QTRY_VERIFY(!viewMenuBarMenu->isVisible());
QTRY_VERIFY(!alignmentSubMenu->isVisible());
QTRY_VERIFY(!verticalSubMenu->isVisible());
}
// Not sure how relevant MenuBar is for touch, but this test is here to make
// sure that only release events cause the menu to open, as:
// - That is how it has always behaved, so maintain that behavior.
// - It's what happens with e.g. overflow menus on Android.
void tst_qquickmenubar::touch()
{
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuWindows);
QQuickControlsApplicationHelper helper(this, QLatin1String("touch.qml"));
QVERIFY2(helper.ready, helper.failureMessage());
centerOnScreen(helper.window);
helper.window->show();
QVERIFY(QTest::qWaitForWindowExposed(helper.window));
QQuickMenuBar *menuBar = helper.window->property("header").value<QQuickMenuBar *>();
QVERIFY(menuBar);
QQuickMenu *fileMenuBarMenu = menuBar->menuAt(0);
QVERIFY(fileMenuBarMenu);
QQuickMenuBarItem *fileMenuBarItem = qobject_cast<QQuickMenuBarItem *>(fileMenuBarMenu->parentItem());
QVERIFY(fileMenuBarItem);
// Trigger a menubar item to open a menu - it should only open on release.
QTest::touchEvent(helper.window, touchScreen.data()).press(0, itemSceneCenter(fileMenuBarItem));
QVERIFY(!fileMenuBarItem->isHighlighted());
QVERIFY(!fileMenuBarMenu->isVisible());
QTest::touchEvent(helper.window, touchScreen.data()).release(0, itemSceneCenter(fileMenuBarItem));
QVERIFY(fileMenuBarItem->isHighlighted());
QVERIFY(fileMenuBarMenu->isVisible());
QTRY_VERIFY(fileMenuBarMenu->isOpened());
}
void tst_qquickmenubar::keys_data()
{
QTest::addColumn<QQuickPopup::PopupType>("popupType");
QTest::newRow("Popup.Item") << QQuickPopup::Item;
// Uncomment when popup windows work 100% (QTBUG-128479)
// if (popupWindowsSupported)
// QTest::newRow("Popup.Window") << QQuickPopup::Window;
}
void tst_qquickmenubar::keys()
{
QFETCH(QQuickPopup::PopupType, popupType);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuWindows);
SKIP_IF_NO_WINDOW_ACTIVATION
QQmlApplicationEngine engine(testFileUrl("menubaritems.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
centerOnScreen(window.data());
moveMouseAway(window.data());
QVERIFY(QTest::qWaitForWindowActive(window.data()));
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
QQuickMenu *fileMenuBarMenu = menuBar->menuAt(0);
QQuickMenu *editMenuBarMenu = menuBar->menuAt(1);
QQuickMenu *viewMenuBarMenu = menuBar->menuAt(2);
QQuickMenu *helpMenuBarMenu = menuBar->menuAt(3);
QVERIFY(fileMenuBarMenu && editMenuBarMenu && viewMenuBarMenu && helpMenuBarMenu);
fileMenuBarMenu->setPopupType(popupType);
editMenuBarMenu->setPopupType(popupType);
viewMenuBarMenu->setPopupType(popupType);
helpMenuBarMenu->setPopupType(popupType);
QQuickMenuBarItem *fileMenuBarItem = qobject_cast<QQuickMenuBarItem *>(fileMenuBarMenu->parentItem());
QQuickMenuBarItem *editMenuBarItem = qobject_cast<QQuickMenuBarItem *>(editMenuBarMenu->parentItem());
QQuickMenuBarItem *viewMenuBarItem = qobject_cast<QQuickMenuBarItem *>(viewMenuBarMenu->parentItem());
QQuickMenuBarItem *helpMenuBarItem = qobject_cast<QQuickMenuBarItem *>(helpMenuBarMenu->parentItem());
QVERIFY(fileMenuBarItem && editMenuBarItem && viewMenuBarItem && helpMenuBarItem);
// trigger a menubar item to open a menu
editMenuBarItem->forceActiveFocus();
QTest::keyClick(window.data(), Qt::Key_Space);
QVERIFY(editMenuBarItem->isHighlighted());
QVERIFY(editMenuBarMenu->isVisible());
QTRY_VERIFY(editMenuBarMenu->isOpened());
QVERIFY(editMenuBarMenu->hasActiveFocus());
// navigate down to the menu
QQuickMenuItem *cutMenuItem = qobject_cast<QQuickMenuItem *>(editMenuBarMenu->itemAt(0));
QVERIFY(cutMenuItem);
QVERIFY(!cutMenuItem->isHighlighted());
QVERIFY(!cutMenuItem->hasActiveFocus());
QTest::keyClick(window.data(), Qt::Key_Down);
QVERIFY(cutMenuItem->isHighlighted());
QVERIFY(cutMenuItem->hasActiveFocus());
// navigate up, back to the menubar
QTest::keyClick(window.data(), Qt::Key_Up);
QTRY_VERIFY(!editMenuBarMenu->isVisible());
QVERIFY(editMenuBarItem->isHighlighted());
QVERIFY(editMenuBarItem->hasActiveFocus());
// There seem to be problems in focus handling in webOS QPA, see https://bugreports.qt.io/browse/WEBOSCI-45
#ifdef Q_OS_WEBOS
QEXPECT_FAIL("", "WEBOSCI-45", Abort);
#endif
QVERIFY(!cutMenuItem->isHighlighted());
QVERIFY(!cutMenuItem->hasActiveFocus());
// navigate down to re-open the menu
QTest::keyClick(window.data(), Qt::Key_Down);
QVERIFY(editMenuBarItem->isHighlighted());
QVERIFY(!editMenuBarItem->hasActiveFocus());
QVERIFY(editMenuBarMenu->isVisible());
QTRY_VERIFY(editMenuBarMenu->isOpened());
QVERIFY(editMenuBarMenu->hasActiveFocus());
QVERIFY(cutMenuItem->isHighlighted());
QVERIFY(cutMenuItem->hasActiveFocus());
// navigate left in popup mode (menu open)
QTest::keyClick(window.data(), Qt::Key_Left);
QVERIFY(fileMenuBarItem->isHighlighted());
QVERIFY(!editMenuBarItem->isHighlighted());
QVERIFY(fileMenuBarMenu->isVisible());
QTRY_VERIFY(fileMenuBarMenu->isOpened());
QTRY_VERIFY(!editMenuBarMenu->isVisible());
// navigate left in popup mode (wrap)
QTest::keyClick(window.data(), Qt::Key_Left);
QVERIFY(helpMenuBarItem->isHighlighted());
QVERIFY(!fileMenuBarItem->isHighlighted());
QVERIFY(helpMenuBarMenu->isVisible());
QTRY_VERIFY(helpMenuBarMenu->isOpened());
QTRY_VERIFY(!fileMenuBarMenu->isVisible());
// navigate up to close the menu
QTest::keyClick(window.data(), Qt::Key_Up);
QVERIFY(helpMenuBarItem->isHighlighted());
QTRY_VERIFY(!helpMenuBarMenu->isVisible());
// navigate right in non-popup mode (wrap)
QTest::keyClick(window.data(), Qt::Key_Right);
QVERIFY(fileMenuBarItem->isHighlighted());
QVERIFY(!helpMenuBarItem->isHighlighted());
QVERIFY(!fileMenuBarMenu->isVisible());
QVERIFY(!helpMenuBarMenu->isVisible());
// navigate right in non-popup mode (menu closed)
QTest::keyClick(window.data(), Qt::Key_Right);
QVERIFY(!fileMenuBarItem->isHighlighted());
QVERIFY(editMenuBarItem->isHighlighted());
QVERIFY(!fileMenuBarMenu->isVisible());
QVERIFY(!editMenuBarMenu->isVisible());
// open a menu
viewMenuBarItem->forceActiveFocus();
QTest::keyClick(window.data(), Qt::Key_Space);
QVERIFY(viewMenuBarItem->isHighlighted());
QVERIFY(viewMenuBarMenu->isVisible());
QTRY_VERIFY(viewMenuBarMenu->isOpened());
QVERIFY(!viewMenuBarItem->hasActiveFocus());
QVERIFY(viewMenuBarMenu->hasActiveFocus());
// open a sub-menu
QQuickMenuItem *alignmentSubMenuItem = qobject_cast<QQuickMenuItem *>(viewMenuBarMenu->itemAt(0));
QVERIFY(alignmentSubMenuItem);
QQuickMenu *alignmentSubMenu = alignmentSubMenuItem->subMenu();
QVERIFY(alignmentSubMenu);
QTest::keyClick(window.data(), Qt::Key_Down);
QVERIFY(alignmentSubMenuItem->isHighlighted());
QVERIFY(!alignmentSubMenu->isVisible());
QTest::keyClick(window.data(), Qt::Key_Right);
QVERIFY(alignmentSubMenu->isVisible());
QTRY_VERIFY(alignmentSubMenu->isOpened());
// open a sub-sub-menu
QQuickMenuItem *horizontalSubMenuItem = qobject_cast<QQuickMenuItem *>(alignmentSubMenu->itemAt(0));
QVERIFY(horizontalSubMenuItem);
QVERIFY(horizontalSubMenuItem->isHighlighted());
QQuickMenu *horizontalSubMenu = horizontalSubMenuItem->subMenu();
QVERIFY(horizontalSubMenu);
QTest::keyClick(window.data(), Qt::Key_Right);
QVERIFY(viewMenuBarMenu->isVisible());
QVERIFY(alignmentSubMenu->isVisible());
QVERIFY(horizontalSubMenu->isVisible());
QTRY_VERIFY(horizontalSubMenu->isOpened());
// navigate left to close a sub-menu
QTest::keyClick(window.data(), Qt::Key_Left);
QTRY_VERIFY(!horizontalSubMenu->isVisible());
QVERIFY(viewMenuBarMenu->isVisible());
QVERIFY(alignmentSubMenu->isVisible());
// navigate right to re-open the sub-menu
QTest::keyClick(window.data(), Qt::Key_Right);
QVERIFY(horizontalSubMenuItem->isHighlighted());
QVERIFY(horizontalSubMenu->isVisible());
QTRY_VERIFY(horizontalSubMenu->isOpened());
// navigate right to the next menubar menu
QTest::keyClick(window.data(), Qt::Key_Right);
QVERIFY(!viewMenuBarItem->isHighlighted());
QVERIFY(helpMenuBarItem->isHighlighted());
QVERIFY(helpMenuBarMenu->isVisible());
QTRY_VERIFY(!viewMenuBarMenu->isVisible());
QTRY_VERIFY(!alignmentSubMenu->isVisible());
QTRY_VERIFY(!horizontalSubMenu->isVisible());
QTRY_VERIFY(helpMenuBarMenu->isOpened());
// navigate back
QTest::keyClick(window.data(), Qt::Key_Left);
QVERIFY(!helpMenuBarItem->isHighlighted());
QVERIFY(viewMenuBarItem->isHighlighted());
QVERIFY(viewMenuBarMenu->isVisible());
QTRY_VERIFY(!helpMenuBarMenu->isVisible());
QTRY_VERIFY(viewMenuBarMenu->isOpened());
// re-open the chain of menus
QTest::keyClick(window.data(), Qt::Key_Down);
QVERIFY(alignmentSubMenuItem->isHighlighted());
QTest::keyClick(window.data(), Qt::Key_Right);
QTRY_VERIFY(alignmentSubMenu->isOpened());
QTest::keyClick(window.data(), Qt::Key_Right);
QTRY_VERIFY(horizontalSubMenu->isOpened());
// repeat escape to close the whole chain of menus one by one
QTest::keyClick(window.data(), Qt::Key_Escape);
QTRY_VERIFY(!horizontalSubMenu->isVisible());
QVERIFY(viewMenuBarItem->isHighlighted());
QVERIFY(viewMenuBarMenu->isVisible());
QVERIFY(alignmentSubMenu->isVisible());
QTest::keyClick(window.data(), Qt::Key_Escape);
QTRY_VERIFY(!alignmentSubMenu->isVisible());
QVERIFY(viewMenuBarItem->isHighlighted());
QVERIFY(viewMenuBarMenu->isVisible());
QTest::keyClick(window.data(), Qt::Key_Escape);
QVERIFY(!viewMenuBarItem->isHighlighted());
QTRY_VERIFY(!viewMenuBarMenu->isVisible());
}
void tst_qquickmenubar::mnemonics_data()
{
QTest::addColumn<QQuickPopup::PopupType>("popupType");
QTest::newRow("Popup.Item") << QQuickPopup::Item;
// Uncomment when popup windows work 100% (QTBUG-128479)
// if (popupWindowsSupported)
// QTest::newRow("Popup.Window") << QQuickPopup::Window;
}
void tst_qquickmenubar::mnemonics()
{
QFETCH(QQuickPopup::PopupType, popupType);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuWindows);
SKIP_IF_NO_WINDOW_ACTIVATION
#if defined(Q_OS_MACOS) or defined(Q_OS_WEBOS)
QSKIP("Mnemonics are not used on this platform");
#endif
QQmlApplicationEngine engine(testFileUrl("menubaritems.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
centerOnScreen(window.data());
moveMouseAway(window.data());
QVERIFY(QTest::qWaitForWindowActive(window.data()));
MnemonicKeySimulator keySim(window.data());
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
QQuickMenu *fileMenuBarMenu = menuBar->menuAt(0);
QQuickMenu *editMenuBarMenu = menuBar->menuAt(1);
QQuickMenu *viewMenuBarMenu = menuBar->menuAt(2);
QQuickMenu *helpMenuBarMenu = menuBar->menuAt(3);
QVERIFY(fileMenuBarMenu && editMenuBarMenu && viewMenuBarMenu && helpMenuBarMenu);
fileMenuBarMenu->setPopupType(popupType);
editMenuBarMenu->setPopupType(popupType);
viewMenuBarMenu->setPopupType(popupType);
helpMenuBarMenu->setPopupType(popupType);
QQuickMenuBarItem *fileMenuBarItem = qobject_cast<QQuickMenuBarItem *>(fileMenuBarMenu->parentItem());
QQuickMenuBarItem *editMenuBarItem = qobject_cast<QQuickMenuBarItem *>(editMenuBarMenu->parentItem());
QQuickMenuBarItem *viewMenuBarItem = qobject_cast<QQuickMenuBarItem *>(viewMenuBarMenu->parentItem());
QQuickMenuBarItem *helpMenuBarItem = qobject_cast<QQuickMenuBarItem *>(helpMenuBarMenu->parentItem());
QVERIFY(fileMenuBarItem && editMenuBarItem && viewMenuBarItem && helpMenuBarItem);
QQuickButton *oopsButton = window->property("oopsButton").value<QQuickButton *>();
QVERIFY(oopsButton);
QSignalSpy oopsButtonSpy(oopsButton, &QQuickButton::clicked);
QVERIFY(oopsButtonSpy.isValid());
// trigger a menubar item to open a menu
keySim.press(Qt::Key_Alt);
keySim.click(Qt::Key_E); // "&Edit"
keySim.release(Qt::Key_Alt);
QVERIFY(editMenuBarItem->isHighlighted());
QVERIFY(!editMenuBarItem->hasActiveFocus());
QVERIFY(editMenuBarMenu->isVisible());
QTRY_VERIFY(editMenuBarMenu->isOpened());
QVERIFY(editMenuBarMenu->hasActiveFocus());
// press Alt to hide the menu
keySim.click(Qt::Key_Alt);
QVERIFY(!editMenuBarItem->isHighlighted());
QVERIFY(!editMenuBarItem->hasActiveFocus());
QVERIFY(!editMenuBarMenu->hasActiveFocus());
QTRY_VERIFY(!editMenuBarMenu->isVisible());
// re-trigger a menubar item to show the menu again
keySim.press(Qt::Key_Alt);
keySim.click(Qt::Key_E); // "&Edit"
keySim.release(Qt::Key_Alt);
QVERIFY(editMenuBarItem->isHighlighted());
QVERIFY(editMenuBarMenu->isVisible());
QTRY_VERIFY(editMenuBarMenu->isOpened());
QVERIFY(editMenuBarMenu->hasActiveFocus());
QVERIFY(!editMenuBarItem->hasActiveFocus());
// trigger another menubar item to open another menu, leave Alt pressed
keySim.press(Qt::Key_Alt);
QTRY_VERIFY(!editMenuBarMenu->isVisible());
keySim.click(Qt::Key_H); // "&Help"
QVERIFY(!editMenuBarItem->isHighlighted());
QVERIFY(helpMenuBarItem->isHighlighted());
QVERIFY(!viewMenuBarMenu->isVisible());
QVERIFY(helpMenuBarMenu->isVisible());
QTRY_VERIFY(helpMenuBarMenu->isOpened());
// trigger a menu item to close the menu
keySim.click(Qt::Key_A); // "&About"
keySim.release(Qt::Key_Alt);
QVERIFY(!helpMenuBarItem->isHighlighted());
QTRY_VERIFY(!helpMenuBarMenu->isVisible());
// trigger a menubar item to open a menu, leave Alt pressed
keySim.press(Qt::Key_Alt);
keySim.click(Qt::Key_V); // "&View"
QVERIFY(!editMenuBarItem->isHighlighted());
QVERIFY(viewMenuBarItem->isHighlighted());
QVERIFY(viewMenuBarMenu->isVisible());
QTRY_VERIFY(viewMenuBarMenu->isOpened());
// trigger a menu item to open a sub-menu, leave Alt pressed
QQuickMenuItem *alignmentSubMenuItem = qobject_cast<QQuickMenuItem *>(viewMenuBarMenu->itemAt(0));
QVERIFY(alignmentSubMenuItem);
QQuickMenu *alignmentSubMenu = alignmentSubMenuItem->subMenu();
QVERIFY(alignmentSubMenu);
keySim.click(Qt::Key_A); // "&Alignment"
#ifndef Q_OS_ANDROID
// On Android sub-menus are not cascading, so the Alignment sub-menu is
// shown instead of View menu.
QVERIFY(viewMenuBarMenu->isVisible());
#endif
QVERIFY(alignmentSubMenu->isVisible());
QTRY_VERIFY(alignmentSubMenu->isOpened());
// trigger a menu item to open a sub-sub-menu, leave Alt pressed
QQuickMenuItem *verticalSubMenuItem = qobject_cast<QQuickMenuItem *>(alignmentSubMenu->itemAt(1));
QVERIFY(verticalSubMenuItem);
QQuickMenu *verticalSubMenu = verticalSubMenuItem->subMenu();
QVERIFY(verticalSubMenu);
keySim.click(Qt::Key_V); // "&Vertical"
#ifndef Q_OS_ANDROID
// On Android sub-menus are not cascading, so the Vertical sub-menu is
// shown instead of View menu and Alignment sub-menu.
QVERIFY(viewMenuBarMenu->isVisible());
QVERIFY(alignmentSubMenu->isVisible());
#endif
QVERIFY(verticalSubMenu->isVisible());
QTRY_VERIFY(verticalSubMenu->isOpened());
// trigger a menu item to close the whole chain of menus
keySim.click(Qt::Key_C); // "&Center"
keySim.release(Qt::Key_Alt);
QVERIFY(!viewMenuBarItem->isHighlighted());
QTRY_VERIFY(!viewMenuBarMenu->isVisible());
QTRY_VERIFY(!alignmentSubMenu->isVisible());
QTRY_VERIFY(!verticalSubMenu->isVisible());
// trigger a menubar item to open a menu, leave Alt pressed
keySim.press(Qt::Key_Alt);
keySim.click(Qt::Key_F); // "&File"
QVERIFY(fileMenuBarItem->isHighlighted());
QVERIFY(fileMenuBarMenu->isVisible());
QTRY_VERIFY(fileMenuBarMenu->isOpened());
QVERIFY(fileMenuBarMenu->hasActiveFocus());
// trigger a menu item to close the menu, which shouldn't trigger a button
// action behind the menu (QTBUG-86276)
QCOMPARE(oopsButtonSpy.size(), 0);
keySim.click(Qt::Key_O); // "&Open..."
keySim.release(Qt::Key_Alt);
QVERIFY(!fileMenuBarItem->isHighlighted());
QVERIFY(!fileMenuBarMenu->isOpened());
QTRY_VERIFY(!fileMenuBarMenu->isVisible());
QCOMPARE(oopsButtonSpy.size(), 0);
// trigger a button action while menu is closed
keySim.press(Qt::Key_Alt);
keySim.click(Qt::Key_O); // "&Oops"
keySim.release(Qt::Key_Alt);
QCOMPARE(oopsButtonSpy.size(), 1);
}
void tst_qquickmenubar::altNavigation()
{
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuWindows);
if (!QGuiApplicationPrivate::platformTheme()->themeHint(QPlatformTheme::MenuBarFocusOnAltPressRelease).toBool())
QSKIP("Menu doesn't get focus via Alt press&release on this platform");
QQmlApplicationEngine engine(testFileUrl("menubaritems.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
centerOnScreen(window.data());
moveMouseAway(window.data());
QVERIFY(QTest::qWaitForWindowActive(window.data()));
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
QQuickMenu *fileMenuBarMenu = menuBar->menuAt(0);
QQuickMenuBarItem *fileMenuBarItem = qobject_cast<QQuickMenuBarItem *>(fileMenuBarMenu->parentItem());
// This logic is somewhat inverted, but QKeyEvent::modifiers() XOR's the modifier
// corresponding to the activated key, so pressing Alt adds the AltModifier, and
// releasing Alt with AltModifier removes the AltModifier.
QTest::keyPress(window.get(), Qt::Key_Alt);
QTest::keyRelease(window.get(), Qt::Key_Alt, Qt::AltModifier);
QVERIFY(menuBar->hasActiveFocus());
QVERIFY(fileMenuBarItem->isHighlighted());
// if menu has focus, pressing the mnemonic without Alt should open the menu
QQuickMenu *editMenuBarMenu = menuBar->menuAt(1);
QQuickMenuBarItem *editMenuBarItem = qobject_cast<QQuickMenuBarItem *>(editMenuBarMenu->parentItem());
QTest::keyPress(window.get(), Qt::Key_E);
QVERIFY(editMenuBarItem->isHighlighted());
QVERIFY(editMenuBarMenu->isVisible());
QTRY_VERIFY(editMenuBarMenu->isOpened());
QVERIFY(editMenuBarMenu->hasActiveFocus());
}
void tst_qquickmenubar::addRemove_data()
{
QTest::addColumn<QString>("testUrl");
QTest::addColumn<bool>("native");
QTest::newRow("menuitems, not native") << QStringLiteral("empty.qml") << false;
if (nativeMenuBarSupported)
QTest::newRow("menuitems, native") << QStringLiteral("empty.qml") << true;
}
void tst_qquickmenubar::addRemove()
{
QFETCH(QString, testUrl);
QFETCH(bool, native);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, !native);
QQmlApplicationEngine engine;
engine.load(testFileUrl(testUrl));
QQuickMenuBar *menuBar = qobject_cast<QQuickMenuBar *>(engine.rootObjects().value(0));
QVERIFY(menuBar);
QQuickMenuBarPrivate *menuBarPrivate = QQuickMenuBarPrivate::get(menuBar);
QCOMPARE(menuBarPrivate->useNativeMenuBar(), native);
if (native)
QVERIFY(menuBarPrivate->nativeHandle());
QQmlComponent component(&engine);
component.setData("import QtQuick.Controls; Menu { }", QUrl());
QPointer<QQuickMenu> menu1(qobject_cast<QQuickMenu *>(component.create()));
QVERIFY(!menu1.isNull());
menuBar->addMenu(menu1.data());
QCOMPARE(menuBar->count(), 1);
QCOMPARE(menuBar->menuAt(0), menu1.data());
QPointer<QQuickMenuBarItem> menuBarItem1(qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(0)));
QVERIFY(menuBarItem1);
QCOMPARE(menuBarItem1->menu(), menu1.data());
QCOMPARE(menuBar->itemAt(0), menuBarItem1.data());
QPointer<QQuickMenu> menu2(qobject_cast<QQuickMenu *>(component.create()));
QVERIFY(!menu2.isNull());
menuBar->insertMenu(0, menu2.data());
QCOMPARE(menuBar->count(), 2);
QCOMPARE(menuBar->menuAt(0), menu2.data());
QCOMPARE(menuBar->menuAt(1), menu1.data());
QPointer<QQuickMenuBarItem> menuBarItem2(qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(0)));
QVERIFY(menuBarItem2);
QCOMPARE(menuBarItem2->menu(), menu2.data());
QCOMPARE(menuBar->itemAt(0), menuBarItem2.data());
QCOMPARE(menuBar->itemAt(1), menuBarItem1.data());
// takeMenu(int) does not explicitly destroy the menu, but leave
// this to the garbage collector. The MenuBarItem, OTOH, is currently
// being destroyed from c++, but this might change in the future.
QCOMPARE(menuBar->takeMenu(1), menu1.data());
QCOMPARE(menuBar->count(), 1);
QVERIFY(!menuBar->menuAt(1));
QVERIFY(!menuBar->itemAt(1));
QTRY_VERIFY(menuBarItem1.isNull());
QVERIFY(!menu1.isNull());
gc(engine);
QVERIFY(!menu1.isNull());
// check that it's safe to call takeMenu(int) with
// an index that is out of range.
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*out of range"));
QCOMPARE(menuBar->takeMenu(-1), nullptr);
QCOMPARE(menuBar->count(), 1);
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*out of range"));
QCOMPARE(menuBar->takeMenu(10), nullptr);
QCOMPARE(menuBar->count(), 1);
// addMenu(Menu) re-creates the respective item in the menubar
menuBar->addMenu(menu1.data());
QCOMPARE(menuBar->count(), 2);
menuBarItem1 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(1));
QVERIFY(!menuBarItem1.isNull());
// removeMenu(menu) does not explicitly destroy the menu, but leave
// this to the garbage collector. The MenuBarItem, OTOH, is currently
// being destroyed from c++, but this might change in the future.
menuBar->removeMenu(menu1.data());
QCOMPARE(menuBar->count(), 1);
QVERIFY(!menuBar->itemAt(1));
QTRY_VERIFY(menuBarItem1.isNull());
QVERIFY(!menu1.isNull());
gc(engine);
QVERIFY(!menu1.isNull());
}
void tst_qquickmenubar::addRemoveInlineMenus_data()
{
QTest::addColumn<bool>("native");
QTest::newRow("not native") << false;
if (nativeMenuBarSupported)
QTest::newRow("native") << true;
}
void tst_qquickmenubar::addRemoveInlineMenus()
{
// Check that it's safe to remove a menu from the menubar, that
// is an inline child from QML (fileMenu). Since it's owned by
// JavaScript, it should be deleted by the gc when appropriate, and
// not upon a call to removeMenu.
QFETCH(bool, native);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, !native);
QQmlApplicationEngine engine;
engine.load(testFileUrl("menus.qml"));
auto window = qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0));
QVERIFY(window);
auto menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
QPointer<QQuickMenu> fileMenu = window->property("fileMenu").value<QQuickMenu *>();
QVERIFY(fileMenu);
QCOMPARE(menuBar->menuAt(0), fileMenu);
QPointer<QQuickItem> menuBarItem = menuBar->itemAt(0);
QVERIFY(menuBarItem);
menuBar->removeMenu(fileMenu);
QVERIFY(menuBar->menuAt(0) != fileMenu);
QTRY_VERIFY(!menuBarItem);
QVERIFY(fileMenu);
gc(engine);
QVERIFY(fileMenu);
// Add it back again, but to the end. This should also be fine, even
// if it no longer matches the initial order in the QML file.
menuBar->addMenu(fileMenu);
QVERIFY(fileMenu);
QCOMPARE(menuBar->menuAt(menuBar->count() - 1), fileMenu);
}
void tst_qquickmenubar::addRemoveMenuFromQml_data()
{
QTest::addColumn<bool>("native");
QTest::newRow("not native") << false;
if (nativeMenuBarSupported)
QTest::newRow("native") << true;
}
void tst_qquickmenubar::addRemoveMenuFromQml()
{
// Create a menu dynamically from QML, and add it to
// the menubar. Remove it again. Check that the
// garbage collector will then destruct it.
QFETCH(bool, native);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, !native);
QQmlApplicationEngine engine;
engine.load(testFileUrl("menus.qml"));
auto window = qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0));
QVERIFY(window);
auto menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
const int initialMenuCount = menuBar->count();
QVERIFY(initialMenuCount > 0);
QMetaObject::invokeMethod(window, "addTestMenu");
QCOMPARE(menuBar->count(), initialMenuCount + 1);
// The "extra" menu should have been added to
// the end of the menu bar. Verify this.
QQuickItem *item = menuBar->itemAt(menuBar->count() - 1);
QPointer<QQuickMenuBarItem> menuBarItem = qobject_cast<QQuickMenuBarItem *>(item);
QVERIFY(menuBarItem);
QPointer<QQuickMenu> menu = menuBar->menuAt(menuBar->count() - 1);
QVERIFY(menu);
QCOMPARE(menu->title(), "extra");
QCOMPARE(menuBarItem->menu(), menu);
// Remove the menu again. Since we have no other references to
// it from QML, it should be collected by the gc.
menuBar->removeMenu(menu);
QCOMPARE(menuBar->count(), initialMenuCount);
QTRY_VERIFY(!menuBarItem);
QVERIFY(menu);
gc(engine);
QVERIFY(!menu);
}
void tst_qquickmenubar::insert_data()
{
QTest::addColumn<bool>("native");
QTest::newRow("not native") << false;
if (nativeMenuBarSupported)
QTest::newRow("native") << true;
}
void tst_qquickmenubar::insert()
{
QFETCH(bool, native);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, !native);
QQmlApplicationEngine engine;
engine.load(testFileUrl("menus.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
const int initialMenuCount = menuBar->count();
QVERIFY(initialMenuCount > 0);
QQmlComponent component(&engine);
component.setData("import QtQuick.Controls; Menu { }", QUrl());
QPointer<QQuickMenu> menu1(qobject_cast<QQuickMenu *>(component.create()));
QVERIFY(!menu1.isNull());
menuBar->insertMenu(0, menu1.data());
QCOMPARE(menuBar->count(), initialMenuCount + 1);
QCOMPARE(menuBar->menuAt(0), menu1.data());
QPointer<QQuickMenu> menu2(qobject_cast<QQuickMenu *>(component.create()));
QVERIFY(!menu2.isNull());
menuBar->insertMenu(2, menu2.data());
QCOMPARE(menuBar->count(), initialMenuCount + 2);
QCOMPARE(menuBar->menuAt(2), menu2.data());
}
void tst_qquickmenubar::showAndHideMenuBarItems_data()
{
QTest::addColumn<bool>("native");
QTest::newRow("not native") << false;
if (nativeMenuBarSupported)
QTest::newRow("native") << true;
}
void tst_qquickmenubar::showAndHideMenuBarItems()
{
// Check that you can toggle MenuBarItem.visible to show and hide menus in the
// menu bar. Note that this is not the same as setting Menu.visible, which will
// instead open or close the menus.
QFETCH(bool, native);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, !native);
QQmlApplicationEngine engine;
engine.load(testFileUrl("showandhide.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
QCOMPARE(menuBar->count(), 4);
auto menuBarItem0 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(0));
auto menuBarItem1 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(1));
auto menuBarItem2 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(2));
auto menuBarItem3 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(3));
// Initially, the three first MenuBarItems are visible, but the 4th is hidden
QVERIFY(menuBarItem0->isVisible());
QVERIFY(menuBarItem1->isVisible());
QVERIFY(menuBarItem2->isVisible());
QVERIFY(!menuBarItem3->isVisible());
// Native and visible QQuickMenus should be backed by
// QPlatformMenus. Otherwise the handle should be nullptr.
QCOMPARE(bool(QQuickMenuPrivate::get(menuBarItem0->menu())->maybeNativeHandle()), native);
QCOMPARE(bool(QQuickMenuPrivate::get(menuBarItem1->menu())->maybeNativeHandle()), native);
QCOMPARE(bool(QQuickMenuPrivate::get(menuBarItem2->menu())->maybeNativeHandle()), native);
QVERIFY(!QQuickMenuPrivate::get(menuBarItem3->menu())->maybeNativeHandle());
// Make the hidden MenuBarItem visible
menuBarItem3->setVisible(true);
QCOMPARE(bool(QQuickMenuPrivate::get(menuBarItem3->menu())->maybeNativeHandle()), native);
QCOMPARE(menuBar->count(), 4);
// Hide it again
menuBarItem3->setVisible(false);
QVERIFY(!QQuickMenuPrivate::get(menuBarItem3->menu())->maybeNativeHandle());
QCOMPARE(menuBar->count(), 4);
// Toggle the visibility of a MenuBarItem created from the
// delegate, which is also initially visible.
menuBarItem0->setVisible(false);
QVERIFY(!QQuickMenuPrivate::get(menuBarItem0->menu())->maybeNativeHandle());
QCOMPARE(menuBar->count(), 4);
// Hide it again
menuBarItem0->setVisible(true);
QCOMPARE(bool(QQuickMenuPrivate::get(menuBarItem0->menu())->maybeNativeHandle()), native);
QCOMPARE(menuBar->count(), 4);
}
void tst_qquickmenubar::removeMenuThatIsOpen()
{
// Check that if we remove a menu that is open, it ends
// up being hidden / closed. This is mostly important for
// non-native menubars.
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuWindows);
QQmlApplicationEngine engine;
engine.load(testFileUrl("menus.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
QQuickMenu *fileMenu = window->property("fileMenu").value<QQuickMenu *>();
QVERIFY(fileMenu);
fileMenu->open();
QVERIFY(fileMenu->isVisible());
menuBar->removeMenu(fileMenu);
QVERIFY(fileMenu);
QTRY_VERIFY(!fileMenu->isVisible());
}
void tst_qquickmenubar::addRemoveExistingMenus_data()
{
QTest::addColumn<bool>("native");
QTest::addColumn<bool>("usePopupWindow");
QTest::newRow("non-native, in-scene") << false << false;
if (nativeMenuBarSupported)
QTest::newRow("native, native") << true << true;
// Uncomment when popup windows work 100%
// if (popupWindowsSupported)
// QTest::newRow("non-native, popup window") << false << true;
}
void tst_qquickmenubar::addRemoveExistingMenus()
{
// Check that you get warnings if trying to add menus that
// are already in the menubar, or remove menus that are not.
QFETCH(bool, native);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, !native);
QQmlApplicationEngine engine;
engine.load(testFileUrl("menus.qml"));
auto window = qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0));
QVERIFY(window);
auto menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
QPointer<QQuickMenu> fileMenu = window->property("fileMenu").value<QQuickMenu *>();
QVERIFY(fileMenu);
QCOMPARE(menuBar->menuAt(0), fileMenu);
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("cannot add menu.*"));
menuBar->addMenu(fileMenu);
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("cannot insert menu.*"));
menuBar->insertMenu(0, fileMenu);
menuBar->removeMenu(fileMenu);
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("cannot remove menu.*"));
menuBar->removeMenu(fileMenu);
}
void tst_qquickmenubar::checkHighlightWhenMenuDismissed_data()
{
QTest::addColumn<QQuickPopup::PopupType>("popupType");
QTest::newRow("Popup.Item") << QQuickPopup::Item;
// Uncomment when popup windows work 100% (QTBUG-128479)
// if (popupWindowsSupported)
// QTest::newRow("Popup.Window") << QQuickPopup::Window;
}
void tst_qquickmenubar::checkHighlightWhenMenuDismissed()
{
SKIP_IF_NO_MOUSE_HOVER;
QFETCH(QQuickPopup::PopupType, popupType);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuWindows);
QQmlApplicationEngine engine(testFileUrl("checkHighlightWhenDismissed.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
centerOnScreen(window.data());
moveMouseAway(window.data());
QVERIFY(QTest::qWaitForWindowExposed(window.data()));
QQuickMenuBar *menuBar = window->findChild<QQuickMenuBar *>("menuBar");
QVERIFY(menuBar);
QQuickMenu *staticMenu = menuBar->menuAt(0);
QQuickMenu *dynamicMenu = menuBar->menuAt(1);
QVERIFY(staticMenu && dynamicMenu);
staticMenu->setPopupType(popupType);
dynamicMenu->setPopupType(popupType);
QQuickMenuBarItem *staticMenuBarItem = qobject_cast<QQuickMenuBarItem *>(staticMenu->parentItem());
QQuickMenuBarItem *dynamicMenuBarItem = qobject_cast<QQuickMenuBarItem *>(dynamicMenu->parentItem());
QVERIFY(staticMenuBarItem && dynamicMenuBarItem);
// highlight the static MenuBarItem and open the menu
QTest::mouseMove(window.data(), staticMenuBarItem->mapToScene(
QPointF(staticMenuBarItem->width() / 2, staticMenuBarItem->height() / 2)).toPoint());
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier,
staticMenuBarItem->mapToScene(QPointF(staticMenuBarItem->width() / 2, staticMenuBarItem->height() / 2)).toPoint());
QVERIFY(staticMenuBarItem->isHighlighted());
QVERIFY(staticMenu->isVisible());
QTRY_VERIFY(staticMenu->isOpened());
// click a menu item to dismiss the menu and unhighlight the static MenuBarItem
QQuickMenuItem *menuItem = qobject_cast<QQuickMenuItem *>(staticMenu->itemAt(0));
QVERIFY(menuItem);
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier,
menuItem->mapToScene(QPointF(menuItem->width() / 2, menuItem->height() / 2)).toPoint());
QVERIFY(!staticMenuBarItem->isHighlighted());
// wait for the menu to be fully gone so that it doesn't interfere with the next test
QTRY_VERIFY(!staticMenu->isVisible());
// highlight the dynamic MenuBarItem and open the menu
QTest::mouseMove(window.data(), dynamicMenuBarItem->mapToScene(
QPointF(dynamicMenuBarItem->width() / 2, dynamicMenuBarItem->height() / 2)).toPoint());
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier,
dynamicMenuBarItem->mapToScene(QPointF(dynamicMenuBarItem->width() / 2, dynamicMenuBarItem->height() / 2)).toPoint());
QVERIFY(dynamicMenuBarItem->isHighlighted());
QVERIFY(dynamicMenu->isVisible());
QTRY_VERIFY(dynamicMenu->isOpened());
// click a menu item to dismiss the menu and unhighlight the dynamic MenuBarItem
menuItem = qobject_cast<QQuickMenuItem *>(dynamicMenu->itemAt(0));
QVERIFY(menuItem);
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier,
menuItem->mapToScene(QPointF(menuItem->width() / 2, menuItem->height() / 2)).toPoint());
QVERIFY(!dynamicMenuBarItem->isHighlighted());
}
void tst_qquickmenubar::hoverAfterClosingWithEscape_data()
{
QTest::addColumn<bool>("usePopupWindow");
QTest::newRow("in-scene popup") << false;
// Uncomment when popup windows work 100%
// if (popupWindowsSupported)
// QTest::newRow("popup window") << true;
}
void tst_qquickmenubar::hoverAfterClosingWithEscape()
{
SKIP_IF_NO_MOUSE_HOVER;
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuWindows);
QQuickControlsApplicationHelper helper(this, QLatin1String("hoverAfterClosingWithEscape.qml"));
QVERIFY2(helper.ready, helper.failureMessage());
QQuickApplicationWindow *window = helper.appWindow;
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
QQuickMenuBar *menuBar = window->findChild<QQuickMenuBar *>("menuBar");
QVERIFY(menuBar);
// Open the first menu by clicking on the first menu bar item.
auto *firstMenuBarItem(qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(0)));
QVERIFY(clickButton(firstMenuBarItem));
QQuickMenu *firstMenu = menuBar->menuAt(0);
QVERIFY(firstMenu);
QTRY_VERIFY(firstMenu->isOpened());
// Close it with the escape key.
QTest::keyClick(window, Qt::Key_Escape);
QTRY_VERIFY(!firstMenu->isVisible());
// Hover over the second menu bar item; it shouldn't cause its menu to open.
auto *secondMenuBarItem(qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(1)));
QTest::mouseMove(window, mapCenterToWindow(secondMenuBarItem));
QQuickMenu *secondMenu = menuBar->menuAt(1);
QVERIFY(secondMenu);
QVERIFY(!secondMenu->isVisible());
}
void tst_qquickmenubar::closeByClickingOutside_data()
{
QTest::addColumn<QQuickPopup::PopupType>("popupType");
QTest::newRow("Item") << QQuickPopup::Item;
QTest::newRow("Window") << QQuickPopup::Window;
}
void tst_qquickmenubar::closeByClickingOutside()
{
SKIP_IF_NO_MOUSE_HOVER;
QFETCH(QQuickPopup::PopupType, popupType);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
QQuickControlsApplicationHelper helper(this, QLatin1String("hoverAfterClosingWithEscape.qml"));
QVERIFY2(helper.ready, helper.failureMessage());
QQuickApplicationWindow *window = helper.appWindow;
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
auto *menuBar = window->property("menuBar").value<QQuickMenuBar*>();
QVERIFY(menuBar);
auto *fileMenu = window->property("fileMenu").value<QQuickMenu*>();
QVERIFY(fileMenu);
auto *firstMenuBarItem(qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(0)));
QVERIFY(firstMenuBarItem);
fileMenu->setPopupType(popupType);
// Open the first menu by clicking on the first menu bar item.
QVERIFY(clickButton(firstMenuBarItem));
QTRY_VERIFY(fileMenu->isOpened());
// Check that the first menu bar item stays highlighted
QVERIFY(firstMenuBarItem->isHighlighted());
// ...also after we move the mouse away from it
const QPoint windowCenter = {window->width() / 2, window->height() / 2};
QTest::mouseMove(window, windowCenter);
QVERIFY(firstMenuBarItem->isHighlighted());
// Close the menu with by clicking outside of it
QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, windowCenter);
QTRY_VERIFY(!fileMenu->isVisible());
// Check that firstMenuBarItem is no longer highlighted since it's
// no longer hovered, and the menu it represents is closed.
QVERIFY(!firstMenuBarItem->isHighlighted());
}
void tst_qquickmenubar::AA_DontUseNativeMenuBar()
{
// Check that we end up with a non-native menu bar when AA_DontUseNativeMenuBar is set.
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
QQmlApplicationEngine engine;
engine.load(testFileUrl("menus.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
auto menuBarPrivate = QQuickMenuBarPrivate::get(menuBar);
QQuickItem *contents = window->property("contents").value<QQuickItem *>();
QVERIFY(contents);
QVERIFY(!menuBarPrivate->nativeHandle());
QVERIFY(menuBar->isVisible());
QVERIFY(menuBar->count() > 0);
QVERIFY(menuBar->height() > 0);
QCOMPARE(contents->height(), window->height() - menuBar->height());
// If the menu bar is not native, the menus should not be native either.
// The main reason for this limitation is that a native menu typically
// run in separate native event loop which will not forward mouse events
// to Qt. And this is needed for a non-native menu bar to work (e.g to
// support hovering over the menu bar items to open and close menus).
const auto firstMenu = menuBar->menuAt(0);
QVERIFY(firstMenu);
QVERIFY(!QQuickMenuPrivate::get(firstMenu)->maybeNativeHandle());
}
void tst_qquickmenubar::AA_DontUseNativeMenuWindows()
{
if (!nativeMenuBarSupported)
QSKIP("this test is only valid when native menu bars are supported!");
// Check that we end up with a native menu bar _with_ native
// menus, even if AA_DontUseNativeMenuWindows is set.
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, false);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuWindows, true);
QQmlApplicationEngine engine;
engine.load(testFileUrl("menus.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
auto menuBarPrivate = QQuickMenuBarPrivate::get(menuBar);
QQuickItem *contents = window->property("contents").value<QQuickItem *>();
QVERIFY(contents);
QVERIFY(menuBarPrivate->nativeHandle());
const auto firstMenu = menuBar->menuAt(0);
QVERIFY(firstMenu);
QVERIFY(QQuickMenuPrivate::get(firstMenu)->maybeNativeHandle());
}
void tst_qquickmenubar::containerItems_data()
{
QTest::addColumn<QString>("testUrl");
QTest::addColumn<bool>("native");
QTest::newRow("menuitems, not native") << QStringLiteral("menubaritems.qml") << false;
QTest::newRow("menus, not native") << QStringLiteral("menus.qml") << false;
if (nativeMenuBarSupported) {
QTest::newRow("menuitems, native") << QStringLiteral("menubaritems.qml") << true;
QTest::newRow("menus, native") << QStringLiteral("menus.qml") << true;
}
}
void tst_qquickmenubar::containerItems()
{
// Check that the MenuBar ends up containing a MenuBarItem
// for each Menu added. This should be the case regardless of
// if the MenuBar is native or not. There are several ways
// of accessing those MenuBarItems and menus in the MenuBar
// API, so check that all end up in sync.
QFETCH(QString, testUrl);
QFETCH(bool, native);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, !native);
QQmlApplicationEngine engine;
engine.load(testFileUrl(testUrl));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
auto *menuBarPrivate = QQuickMenuBarPrivate::get(menuBar);
QCOMPARE(menuBarPrivate->useNativeMenuBar(), native);
QCOMPARE(menuBar->count(), 4);
for (int i = 0; i < menuBar->count(); ++i) {
QQuickMenu *menu = menuBar->menuAt(i);
QVERIFY(menu);
// Test the itemAt() API
QQuickItem *item = menuBar->itemAt(i);
QVERIFY(item);
auto menuBarItem = qobject_cast<QQuickMenuBarItem *>(item);
QVERIFY(menuBarItem);
QCOMPARE(menuBarItem->menu(), menu);
// Test the "contentData" list property API
auto cd = menuBarPrivate->contentData();
QCOMPARE(cd.count(&cd), menuBar->count());
auto cdItem = static_cast<QQuickItem *>(cd.at(&cd, i));
QVERIFY(cdItem);
auto cdMenuBarItem = qobject_cast<QQuickMenuBarItem *>(cdItem);
QVERIFY(cdMenuBarItem);
QCOMPARE(cdMenuBarItem->menu(), menu);
// Test the "menus" list property API
auto menus = QQuickMenuBarPrivate::get(menuBar)->menus();
QCOMPARE(menus.count(&menus), menuBar->count());
auto menusMenu = menus.at(&menus, i);
QVERIFY(menusMenu);
QCOMPARE(menusMenu, menu);
}
}
void tst_qquickmenubar::mixedContainerItems_data()
{
QTest::addColumn<bool>("native");
QTest::newRow("not native") << false;
if (nativeMenuBarSupported)
QTest::newRow("native") << true;
}
void tst_qquickmenubar::mixedContainerItems()
{
// The application is allowed to add items other
// than MenuBarItems and Menus as children. But those
// should just be ignored by the MenuBar (and the Container).
QFETCH(bool, native);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, !native);
QQmlApplicationEngine engine;
engine.load(testFileUrl("mixed.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
// The menubar has four children, but only three of them are
// Menus and MenuBarItems. So we should therefore only end up
// with three menus in the MenuBar, and three items in the Container.
QCOMPARE(menuBar->count(), 3);
for (int i = 0; i < 3; ++i) {
auto item = menuBar->itemAt(i);
QVERIFY(item);
auto menuBarItem = qobject_cast<QQuickMenuBarItem *>(item);
QVERIFY(menuBarItem);
QCOMPARE(menuBarItem->menu(), menuBar->menuAt(i));
}
// Try to add an unsupported item dynamically. It should
// have no impact on the MenuBar/Container API.
QQmlComponent component(&engine);
component.setData("import QtQuick; Item { }", QUrl());
QPointer<QQuickItem> plainItem(qobject_cast<QQuickItem *>(component.create()));
QVERIFY(plainItem);
menuBar->addItem(plainItem);
QCOMPARE(menuBar->count(), 3);
for (int i = 0; i < 3; ++i) {
auto item = menuBar->itemAt(i);
QVERIFY(item);
auto menuBarItem = qobject_cast<QQuickMenuBarItem *>(item);
QVERIFY(menuBarItem);
QCOMPARE(menuBarItem->menu(), menuBar->menuAt(i));
}
// Remove it again. It should have no impact on
// the MenuBar/Container API.
menuBar->removeItem(plainItem);
QCOMPARE(menuBar->count(), 3);
for (int i = 0; i < 3; ++i) {
auto item = menuBar->itemAt(i);
QVERIFY(item);
auto menuBarItem = qobject_cast<QQuickMenuBarItem *>(item);
QVERIFY(menuBarItem);
QCOMPARE(menuBarItem->menu(), menuBar->menuAt(i));
}
}
void tst_qquickmenubar::applicationWindow_data()
{
QTest::addColumn<bool>("initiallyNative");
QTest::addColumn<bool>("initiallyVisible");
QTest::newRow("initially not native, visible") << false << true;
QTest::newRow("initially not native, hidden") << false << false;
if (nativeMenuBarSupported) {
QTest::newRow("initially native, visible") << true << true;
QTest::newRow("initially native, hidden") << true << false;
}
}
void tst_qquickmenubar::applicationWindow()
{
// Check that ApplicationWindow adds or removes the non-native
// menubar in response to toggling Qt::AA_DontUseNativeMenuBar and
// MenuBar.visible.
QFETCH(bool, initiallyNative);
QFETCH(bool, initiallyVisible);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, !initiallyNative);
QQmlApplicationEngine engine;
engine.setInitialProperties({{ "visible", initiallyVisible }});
engine.load(testFileUrl("menus.qml"));
QPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
auto menuBarPrivate = QQuickMenuBarPrivate::get(menuBar);
QQuickItem *contents = window->property("contents").value<QQuickItem *>();
QVERIFY(contents);
for (const bool visible : {initiallyVisible, !initiallyVisible, initiallyVisible}) {
menuBar->setVisible(visible);
const bool nativeMenuBarVisible = bool(menuBarPrivate->nativeHandle());
QCOMPARE(nativeMenuBarVisible, initiallyNative && visible);
if (!visible) {
QVERIFY(!menuBar->isVisible());
QVERIFY(!nativeMenuBarVisible);
QCOMPARE(contents->height(), window->height());
} else if (nativeMenuBarVisible) {
QVERIFY(menuBar->isVisible());
QCOMPARE(contents->height(), window->height());
} else {
QVERIFY(menuBar->isVisible());
QVERIFY(menuBar->height() > 0);
QCOMPARE(contents->height(), window->height() - menuBar->height());
}
}
}
void tst_qquickmenubar::menubarAsHeader_data()
{
QTest::addColumn<bool>("native");
QTest::newRow("not native") << false;
if (nativeMenuBarSupported)
QTest::newRow("native") << true;
}
void tst_qquickmenubar::menubarAsHeader()
{
// ApplicationWindow.menuBar was added in Qt 5.10. Before that
// the menuBar was supposed to be assigned to ApplicationWindow.header.
// For backwards compatibility, check that you can still do that.
QFETCH(bool, native);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, !native);
QQmlApplicationEngine engine;
engine.load(testFileUrl("menubarAsHeader.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
QQuickMenuBar *menuBar = window->property("header").value<QQuickMenuBar *>();
QVERIFY(menuBar);
auto menuBarPrivate = QQuickMenuBarPrivate::get(menuBar);
QQuickItem *contents = window->property("contents").value<QQuickItem *>();
QVERIFY(contents);
QVERIFY(menuBar->count() > 0);
QCOMPARE(menuBarPrivate->nativeHandle() != nullptr, native);
if (menuBarPrivate->nativeHandle()) {
// Using native menubar
QCOMPARE(contents->height(), window->height());
} else {
// Not using native menubar
QCOMPARE(contents->height(), window->height() - menuBar->height());
}
}
void tst_qquickmenubar::menuPosition_data()
{
QTest::addColumn<QQuickPopup::PopupType>("popupType");
QTest::newRow("Popup.Item") << QQuickPopup::Item;
if (popupWindowsSupported)
QTest::newRow("Popup.Window") << QQuickPopup::Window;
}
static bool pixelsCloseEnough(int lhs, int rhs)
{
return qAbs(lhs - rhs) < 2;
}
void tst_qquickmenubar::menuPosition()
{
QFETCH(QQuickPopup::PopupType, popupType);
// A Menu.qml will typically have a background with a drop-shadow. And to make
// room for this shadow, the Menu itself is made bigger by using Control.insets.
// This will make room for both the background and its shadow.
// To make sure that the corner of the background (rather than the shadow) ends up
// at the requested menu position, the effective position of the menu will be
// shifted a bit up and left. This test will therefore check that the corner of the
// background ends up that the requested position.
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, true);
// Use in-scene popups for this test, since we have no guarantee where a window
// manager might end up placing a menu.
QQmlApplicationEngine engine;
engine.load(testFileUrl("menus.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
QQuickMenu *editMenu = menuBar->menuAt(1);
QVERIFY(editMenu);
QQuickMenuPrivate *editMenuPrivate = QQuickMenuPrivate::get(editMenu);
const QPoint requestedPos {50, 50};
const QPointF insetAdjustments {-editMenu->leftInset(), -editMenu->topInset()};
editMenu->setPopupType(popupType);
editMenu->setX(requestedPos.x());
editMenu->setY(requestedPos.y());
editMenu->setVisible(true);
QTRY_VERIFY(editMenu->isOpened());
if (editMenuPrivate->usePopupWindow()) {
QTRY_VERIFY(editMenuPrivate->popupWindow);
QVERIFY(QTest::qWaitForWindowExposed(editMenuPrivate->popupWindow));
}
static const QString errorString1("Expected %1, was %2");
QVERIFY2(pixelsCloseEnough(editMenu->x(), requestedPos.x()), qPrintable(errorString1.arg(requestedPos.x()).arg(editMenu->x())));
QVERIFY2(pixelsCloseEnough(editMenu->y(), requestedPos.y()), qPrintable(errorString1.arg(requestedPos.y()).arg(editMenu->y())));
QQuickItem *background = editMenu->background();
QVERIFY(background);
static const QString errorString2("The background's %1 coordinate changed when mapped to the overlay's coordinate space. %2");
const QPoint bgPos = editMenu->parentItem()->mapFromGlobal(background->mapToGlobal(insetAdjustments)).toPoint();
QVERIFY2(pixelsCloseEnough(requestedPos.x(), bgPos.x()), qPrintable(errorString2.arg("x").arg(errorString1.arg(requestedPos.x()).arg(bgPos.x()))));
QVERIFY2(pixelsCloseEnough(requestedPos.y(), bgPos.y()), qPrintable(errorString2.arg("y").arg(errorString1.arg(requestedPos.y()).arg(bgPos.y()))));
}
void tst_qquickmenubar::changeDelegate_data()
{
QTest::addColumn<bool>("native");
QTest::newRow("not native") << false;
if (nativeMenuBarSupported)
QTest::newRow("native") << true;
}
void tst_qquickmenubar::changeDelegate()
{
// Check that you can change the delegate, and that this
// will produce new delegate items, except for the MenuBarItem
// that is created inline in the QML code, and hence doesn't use the delegate.
QFETCH(bool, native);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, !native);
QQmlApplicationEngine engine;
engine.load(testFileUrl("nodelegate.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
QCOMPARE(menuBar->count(), 3);
QQmlComponent delegate1(&engine);
delegate1.setData("import QtQuick.Controls; MenuBarItem {}", QUrl());
menuBar->setDelegate(&delegate1);
auto menuBarItem0_v1 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(0));
auto menuBarItem1_v1 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(1));
auto menuBarItem2_v1 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(2));
QVERIFY(menuBarItem0_v1);
QVERIFY(menuBarItem1_v1);
QVERIFY(menuBarItem2_v1);
QVERIFY(menuBarItem0_v1->isVisible());
QVERIFY(menuBarItem1_v1->isVisible());
QVERIFY(menuBarItem2_v1->isVisible());
QVERIFY(menuBarItem0_v1->menu());
QVERIFY(menuBarItem1_v1->menu());
QVERIFY(menuBarItem2_v1->menu());
QCOMPARE(menuBar->menuAt(0), menuBarItem0_v1->menu());
QCOMPARE(menuBar->menuAt(1), menuBarItem1_v1->menu());
QCOMPARE(menuBar->menuAt(2), menuBarItem2_v1->menu());
// Change the delegate
QQmlComponent delegate2(&engine);
delegate2.setData("import QtQuick.Controls; MenuBarItem {}", QUrl());
menuBar->setDelegate(&delegate2);
auto menuBarItem0_v2 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(0));
auto menuBarItem1_v2 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(1));
auto menuBarItem2_v2 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(2));
QVERIFY(menuBarItem0_v2);
QVERIFY(menuBarItem1_v2);
QVERIFY(menuBarItem2_v2);
// The delegate items should now have changed, except for
// menuBarItem2, which is not created from the delegate.
QVERIFY(menuBarItem0_v2 != menuBarItem0_v1);
QVERIFY(menuBarItem1_v2 != menuBarItem1_v1);
QCOMPARE(menuBarItem2_v2, menuBarItem2_v1);
QVERIFY(menuBarItem0_v2->isVisible());
QVERIFY(menuBarItem1_v2->isVisible());
QVERIFY(menuBarItem2_v2->isVisible());
QVERIFY(menuBarItem0_v2->menu());
QVERIFY(menuBarItem1_v2->menu());
QVERIFY(menuBarItem2_v2->menu());
QCOMPARE(menuBar->menuAt(0), menuBarItem0_v2->menu());
QCOMPARE(menuBar->menuAt(1), menuBarItem1_v2->menu());
QCOMPARE(menuBar->menuAt(2), menuBarItem2_v2->menu());
}
void tst_qquickmenubar::invalidDelegate_data()
{
QTest::addColumn<bool>("native");
QTest::addColumn<bool>("useInvalidDelegate");
QTest::newRow("not native, no delegate") << false << false;
QTest::newRow("not native, invalid delegate") << false << true;
if (nativeMenuBarSupported) {
QTest::newRow("native, no delegate") << true << false;
QTest::newRow("native, invalid delegate") << true << true;
}
}
void tst_qquickmenubar::invalidDelegate()
{
// Check that QQuickMenuBar can handle a delegate that is either null, or not a
// MenuBarItem. The former won't produce any warnings, but the latter should.
// In either case, this will not produce visible menus in the menu bar, except
// for the menus that are wrapped inside inline MenuBarItems, and therefore
// not using the delegate.
// To ensure that we still bookkeep the menus for the failing delegates, in case
// the delegate changes later, and that functions such as menuAt(index) continues
// to work, hidden placeholder MenuBarItems will be used instead.
QFETCH(bool, native);
QFETCH(bool, useInvalidDelegate);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, !native);
QQmlApplicationEngine engine;
if (useInvalidDelegate) {
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("cannot insert menu.*"));
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("cannot insert menu.*"));
}
if (useInvalidDelegate)
engine.load(testFileUrl("invaliddelegate.qml"));
else
engine.load(testFileUrl("nodelegate.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
QCOMPARE(menuBar->count(), 3);
// Menu 2 is an inline MenuBarItem, and is unaffected by the delegate
auto inlineMenuBarItem = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(2));
for (int i = 0; i <= 2; ++i) {
auto menuBarItem = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(i));
QVERIFY(menuBarItem);
auto menu = menuBarItem->menu();
QVERIFY(menu);
QCOMPARE(menu, menuBar->menuAt(i));
if (menuBarItem == inlineMenuBarItem) {
QVERIFY(menuBarItem->isVisible());
QCOMPARE(bool(QQuickMenuPrivate::get(menu)->maybeNativeHandle()), native);
} else {
// Menus created from the invalid delegate should be hidden. They should also
// not have a native handle, since they should not be in a native menu bar.
QVERIFY(!menuBarItem->isVisible());
QVERIFY(!bool(QQuickMenuPrivate::get(menu)->maybeNativeHandle()));
}
}
// Add a new menu. This one should also be inserted into a placeholder MenuBarItem
if (useInvalidDelegate)
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("cannot insert menu.*"));
QQmlComponent component(&engine);
component.setData("import QtQuick.Controls; Menu { }", QUrl());
auto menu = qobject_cast<QQuickMenu *>(component.create());
QVERIFY(menu);
menuBar->addMenu(menu);
QCOMPARE(menuBar->count(), 4);
auto menuBarItem3 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(3));
QVERIFY(menuBarItem3);
QVERIFY(!menuBarItem3->isVisible());
QCOMPARE(menuBar->menuAt(3), menu);
QCOMPARE(menuBar->menuAt(3), menuBarItem3->menu());
QVERIFY(!QQuickMenuPrivate::get(menu)->maybeNativeHandle());
// Finally, set a valid delegate. This will make all MenuBarItems visible.
QQmlComponent delegate(&engine);
delegate.setData("import QtQuick.Controls; MenuBarItem { }", QUrl());
menuBar->setDelegate(&delegate);
for (int i = 0; i <= 3; ++i) {
auto menuBarItem = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(i));
QVERIFY(menuBarItem);
QVERIFY(menuBarItem->isVisible());
auto menu = menuBarItem->menu();
QVERIFY(menu);
QCOMPARE(menu, menuBar->menuAt(i));
QCOMPARE(bool(QQuickMenuPrivate::get(menu)->maybeNativeHandle()), native);
}
// inlineMenuBarItem was not created from a delegate, and shouldn't change
QCOMPARE(qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(2)), inlineMenuBarItem);
}
void tst_qquickmenubar::panMenuBar_data()
{
QTest::addColumn<bool>("usePopupWindow");
QTest::newRow("in-scene popup") << false;
// Uncomment when popup windows work 100%
// if (popupWindowsSupported)
// QTest::newRow("popup window") << true;
}
void tst_qquickmenubar::panMenuBar()
{
// Check that a MenuBarItem's menu opens when you click it. And then check that
// if you hover the next MenuBarItem in the MenuBar, that the first one will
// close, and the second one will open.
#if !defined(Q_OS_MACOS) || !defined(Q_OS_WINDOWS)
QSKIP("This test doesn't pass on e.g QNX. It needs more investigation before it can be enabled");
#endif
#ifdef Q_OS_ANDROID
// Android theme does not use hover effects, so moving the mouse would not
// highlight an item
QSKIP("Panning of MenuBar not supported");
#endif
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, true);
QQmlApplicationEngine engine;
engine.load(testFileUrl("menus.qml"));
QScopedPointer<QQuickApplicationWindow> window(qobject_cast<QQuickApplicationWindow *>(engine.rootObjects().value(0)));
QVERIFY(window);
QQuickMenuBar *menuBar = window->property("menuBar").value<QQuickMenuBar *>();
QVERIFY(menuBar);
QQuickMenuBarPrivate *menuBar_d = QQuickMenuBarPrivate::get(menuBar);
auto menuBarItem0 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(0));
auto menuBarItem1 = qobject_cast<QQuickMenuBarItem *>(menuBar->itemAt(1));
QVERIFY(menuBarItem0);
QVERIFY(menuBarItem1);
QTest::mouseClick(window.data(), Qt::LeftButton, Qt::NoModifier, itemSceneCenter(menuBarItem0));
QVERIFY(menuBarItem0->isHighlighted());
QVERIFY(!menuBarItem1->isHighlighted());
QCOMPARE(menuBar_d->currentItem, menuBarItem0);
QVERIFY(menuBar_d->currentMenuOpen);
QTRY_VERIFY(menuBarItem0->menu()->isOpened());
QTest::mouseMove(window.data(), itemSceneCenter(menuBarItem1));
QVERIFY(!menuBarItem0->isHighlighted());
QVERIFY(menuBarItem1->isHighlighted());
QCOMPARE(menuBar_d->currentItem, menuBarItem1);
QVERIFY(menuBar_d->currentMenuOpen);
QTRY_VERIFY(menuBarItem1->menu()->isOpened());
QTRY_VERIFY(!menuBarItem0->menu()->isOpened());
}
QTEST_QUICKCONTROLS_MAIN(tst_qquickmenubar)
#include "tst_qquickmenubar.moc"