diff --git a/framework/uicomponents/qml/Muse/UiComponents/menuview.cpp b/framework/uicomponents/qml/Muse/UiComponents/menuview.cpp index 0fe4ead2da..46afc705dd 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/menuview.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/menuview.cpp @@ -220,8 +220,8 @@ void MenuView::updateGeometry() setCascadeAlign(Qt::AlignmentFlag::AlignLeft); newPopupPos = PopupPosition::Right; } else { - // move to the left to an area that doesn't fit - movePos(m_globalPos.x() - (viewRect.right() - paddedAnchorRect.right()) + padding() * 2, m_globalPos.y()); + // Place the menu to the left of the cursor instead of pinning it to the screen edge + movePos(parentTopLeft.x() - viewRect.width() + padding() * 2, m_globalPos.y()); } } diff --git a/framework/uicomponents/qml/Muse/UiComponents/menuview.h b/framework/uicomponents/qml/Muse/UiComponents/menuview.h index d932cda2da..ea13eedeae 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/menuview.h +++ b/framework/uicomponents/qml/Muse/UiComponents/menuview.h @@ -26,6 +26,8 @@ #include "popupview.h" namespace muse::uicomponents { +class MenuViewTestAccess; + class MenuView : public PopupView { Q_OBJECT @@ -84,6 +86,8 @@ public slots: void cascadeAlignChanged(Qt::AlignmentFlag cascadeAlign); private: + friend class MenuViewTestAccess; + void componentComplete() override; void updateGeometry() override; diff --git a/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt b/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt index f2fe475ca8..7a25b5f705 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt +++ b/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt @@ -23,6 +23,7 @@ set(MODULE_TEST muse_uicomponents_qml_tests) set(MODULE_TEST_SRC ${CMAKE_CURRENT_LIST_DIR}/doubleinputvalidator_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/intinputvalidator_tests.cpp + ${CMAKE_CURRENT_LIST_DIR}/menuview_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/sectionproxymodel_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/sortfilterproxymodel_tests.cpp @@ -31,6 +32,7 @@ set(MODULE_TEST_SRC set(MODULE_TEST_LINK Qt::Qml + Qt::Quick muse_uicomponents_qml ) diff --git a/framework/uicomponents/qml/Muse/UiComponents/tests/menuview_tests.cpp b/framework/uicomponents/qml/Muse/UiComponents/tests/menuview_tests.cpp new file mode 100644 index 0000000000..dcfaa19b56 --- /dev/null +++ b/framework/uicomponents/qml/Muse/UiComponents/tests/menuview_tests.cpp @@ -0,0 +1,143 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include + +#include +#include +#include +#include + +#include "../menuview.h" + +using namespace muse::uicomponents; + +namespace muse::uicomponents { +class MenuViewTestAccess +{ +public: + static void updateGeometry(MenuView& view) + { + view.updateGeometry(); + } +}; +} + +namespace { +class MainWindowStub final : public muse::ui::IMainWindow +{ +public: + void init(muse::ui::MainWindowBridge*) override {} + void deinit() override {} + + QWindow* qWindow() const override { return nullptr; } + + void requestShowOnBack() override {} + void requestShowOnFront() override {} + + bool isFullScreen() const override { return false; } + muse::async::Notification isFullScreenChanged() const override { return {}; } + void toggleFullScreen() override {} + + QScreen* screen() const override { return QGuiApplication::primaryScreen(); } +}; + +class TestMenuView final : public MenuView +{ +public: + using MenuView::MenuView; + + QPointF globalPosition() const { return m_globalPos; } + + void setMainWindowForTest(const std::shared_ptr& window) + { + mainWindow.set(window); + } +}; +} + +TEST(MenuViewTests, OverflowingTopLevelMenuOpensLeftOfCursor) +{ + QScreen* screen = QGuiApplication::primaryScreen(); + ASSERT_NE(screen, nullptr); + + const QRect availableGeometry = screen->availableGeometry(); + ASSERT_GT(availableGeometry.width(), 400); + + QQuickWindow parentWindow; + parentWindow.setGeometry(availableGeometry); + + QQuickItem anchor(parentWindow.contentItem()); + anchor.setX(availableGeometry.width() - 100); + anchor.setY(100); + anchor.setSize(QSizeF(1, 1)); + + QQuickItem content; + TestMenuView menu(&anchor); + menu.setMainWindowForTest(std::make_shared()); + menu.setContentItem(&content); + menu.setDesiredWidth(200); + menu.setDesiredHeight(100); + + MenuViewTestAccess::updateGeometry(menu); + + const qreal cursorX = anchor.mapToGlobal(QPointF(0, 0)).x(); + const qreal contentRight = menu.globalPosition().x() + menu.padding() + menu.desiredWidth(); + + EXPECT_LT(menu.globalPosition().x(), cursorX); + EXPECT_DOUBLE_EQ(contentRight, cursorX); + EXPECT_NE(contentRight, availableGeometry.right()); +} + +TEST(MenuViewTests, TopLevelMenuUsesScreenBoundaryInsteadOfParentWindow) +{ + QScreen* screen = QGuiApplication::primaryScreen(); + ASSERT_NE(screen, nullptr); + + const QRect availableGeometry = screen->availableGeometry(); + ASSERT_GT(availableGeometry.width(), 400); + + QQuickWindow parentWindow; + parentWindow.setGeometry(availableGeometry.x(), availableGeometry.y(), availableGeometry.width() / 2, + availableGeometry.height()); + + QQuickItem anchor(parentWindow.contentItem()); + anchor.setX(parentWindow.width() - 50); + anchor.setY(100); + anchor.setSize(QSizeF(1, 1)); + + QQuickItem content; + TestMenuView menu(&anchor); + menu.setMainWindowForTest(std::make_shared()); + menu.setContentItem(&content); + menu.setDesiredWidth(200); + menu.setDesiredHeight(100); + + MenuViewTestAccess::updateGeometry(menu); + + const qreal contentRight = menu.globalPosition().x() + menu.padding() + menu.desiredWidth(); + + EXPECT_GT(contentRight, parentWindow.geometry().right()); + EXPECT_LE(contentRight, availableGeometry.right()); +}