LCOV - code coverage report
Current view: top level - muse/framework/uicomponents/qml/Muse/UiComponents - menuview.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 0.0 % 159 0
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2021 MuseScore Limited and others
       9              :  *
      10              :  * This program is free software: you can redistribute it and/or modify
      11              :  * it under the terms of the GNU General Public License version 3 as
      12              :  * published by the Free Software Foundation.
      13              :  *
      14              :  * This program is distributed in the hope that it will be useful,
      15              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      16              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17              :  * GNU General Public License for more details.
      18              :  *
      19              :  * You should have received a copy of the GNU General Public License
      20              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      21              :  */
      22              : 
      23              : #include "menuview.h"
      24              : 
      25              : #include "log.h"
      26              : 
      27              : using namespace muse::uicomponents;
      28              : 
      29              : static const QString MENU_VIEW_CONTENT_OBJECT_NAME("_MenuViewContent");
      30              : 
      31              : // Padding so that our menus don't "collide" with the top/bottom of our screen...
      32              : static const int TOP_BOTTOM_EDGE_PADDING = 16;
      33              : 
      34            0 : MenuView::MenuView(QQuickItem* parent)
      35            0 :     : PopupView(parent)
      36              : {
      37            0 :     setObjectName("MenuView");
      38              : 
      39            0 :     setShowArrow(false);
      40            0 :     setPadding(8);
      41            0 : }
      42              : 
      43            0 : bool MenuView::isSearchable() const
      44              : {
      45            0 :     return m_isSearchable;
      46              : }
      47              : 
      48            0 : void MenuView::setIsSearchable(bool isSearchable)
      49              : {
      50            0 :     if (m_isSearchable == isSearchable) {
      51            0 :         return;
      52              :     }
      53            0 :     m_isSearchable = isSearchable;
      54            0 :     emit isSearchableChanged();
      55              : }
      56              : 
      57            0 : bool MenuView::isSearching() const
      58              : {
      59            0 :     return m_isSearching;
      60              : }
      61              : 
      62            0 : void MenuView::setIsSearching(bool isSearching)
      63              : {
      64            0 :     IF_ASSERT_FAILED(m_isSearchable || !isSearching) {
      65            0 :         return;
      66              :     }
      67            0 :     if (m_isSearching == isSearching) {
      68            0 :         return;
      69              :     }
      70            0 :     m_isSearching = isSearching;
      71            0 :     emit isSearchingChanged();
      72              : }
      73              : 
      74            0 : int MenuView::viewMargins() const
      75              : {
      76            0 :     return 4;
      77              : }
      78              : 
      79            0 : Qt::AlignmentFlag MenuView::cascadeAlign() const
      80              : {
      81            0 :     return m_cascadeAlign;
      82              : }
      83              : 
      84            0 : void MenuView::setCascadeAlign(Qt::AlignmentFlag cascadeAlign)
      85              : {
      86            0 :     if (m_cascadeAlign == cascadeAlign) {
      87            0 :         return;
      88              :     }
      89              : 
      90            0 :     m_cascadeAlign = cascadeAlign;
      91            0 :     emit cascadeAlignChanged(m_cascadeAlign);
      92              : }
      93              : 
      94            0 : void MenuView::componentComplete()
      95              : {
      96            0 :     m_contentItem->setObjectName(m_contentItem->objectName() + MENU_VIEW_CONTENT_OBJECT_NAME);
      97              : 
      98            0 :     PopupView::componentComplete();
      99            0 : }
     100              : 
     101            0 : void MenuView::updateGeometry()
     102              : {
     103              :     // Desired height & width come from QML - these represent the hypothetical size of the
     104              :     // menu if there were no restrictions from the screen...
     105            0 :     setContentHeight(m_desiredHeight);
     106            0 :     setContentWidth(m_desiredWidth);
     107              : 
     108            0 :     const QQuickItem* parent = parentItem();
     109            0 :     IF_ASSERT_FAILED(parent) {
     110            0 :         return;
     111              :     }
     112              : 
     113            0 :     const QPointF parentTopLeft = parent->mapToGlobal(QPoint(0, 0));
     114              : 
     115            0 :     if (m_globalPos.isNull()) {
     116            0 :         m_globalPos = parentTopLeft;
     117              :     }
     118              : 
     119            0 :     const QRectF paddedAnchorRect = anchorGeometry().adjusted(0, TOP_BOTTOM_EDGE_PADDING, 0, -TOP_BOTTOM_EDGE_PADDING);
     120            0 :     QRectF viewRect = viewGeometry();
     121              : 
     122              :     //! NOTE: should be after resolving anchor geometry
     123              :     //! because we can move out of the screen
     124            0 :     m_globalPos += m_localPos;
     125              : 
     126            0 :     PopupPosition::Type newPopupPos = popupPosition();
     127            0 :     setCascadeAlign(Qt::AlignmentFlag::AlignRight);
     128              : 
     129            0 :     const auto movePos = [this, &viewRect](qreal x, qreal y) {
     130            0 :         m_globalPos.setX(x);
     131            0 :         m_globalPos.setY(y);
     132              : 
     133            0 :         viewRect.moveTopLeft(m_globalPos);
     134            0 :     };
     135              : 
     136              :     // Shrink and move the popup down if we detect an overlap with the top of the anchor rect...
     137            0 :     const auto clampTopIfNeed = [&]() {
     138            0 :         const qreal topOverlap = paddedAnchorRect.top() - viewRect.y();
     139            0 :         if (topOverlap > 0) {
     140            0 :             setContentHeight(m_desiredHeight - topOverlap);
     141            0 :             movePos(m_globalPos.x(), m_globalPos.y() + topOverlap);
     142              :         }
     143            0 :     };
     144              : 
     145            0 :     const QQuickItem* parentMenuContentItem = this->parentMenuContentItem();
     146            0 :     const bool isCascade = parentMenuContentItem != nullptr;
     147              : 
     148            0 :     if (isCascade) {
     149              :         // Position submenus to the right of the parent, at the same height...
     150            0 :         movePos(parentTopLeft.x() + parent->width(), m_globalPos.y() - parent->height() - viewMargins());
     151              :     }
     152              : 
     153            0 :     if (viewRect.left() < paddedAnchorRect.left()) { // The left of this menu overlaps the left of the anchor (doesn't fit)...
     154            0 :         movePos(m_globalPos.x() + paddedAnchorRect.left() - viewRect.left(), m_globalPos.y()); // Move to the right
     155              :     }
     156              : 
     157            0 :     const bool isSearchingAbove = isSearching() && popupPosition() == PopupPosition::Top;
     158            0 :     const bool isSearchingRight = isSearching() && popupPosition() == PopupPosition::Right;
     159            0 :     const bool isSearchingBottom = isSearching() && popupPosition() == PopupPosition::Bottom;
     160              : 
     161            0 :     const auto doRepositionResize = [&]() { // This gets quite complicated - lambda avoids nesting...
     162              :         // See usage of this lambda - it should only be called if we're searching or if we've detected
     163              :         // an overlap with the bottom of the anchor rect...
     164              : 
     165            0 :         const qreal bottomOverlap = viewRect.bottom() - paddedAnchorRect.bottom();
     166              : 
     167            0 :         if (isCascade) {
     168              :             // Submenu overlaps bottom - move up by overlap amount (don't change X)...
     169            0 :             movePos(m_globalPos.x(), m_globalPos.y() - bottomOverlap);
     170            0 :             clampTopIfNeed();
     171            0 :             return;
     172              :         }
     173              : 
     174            0 :         if (isSearchingBottom) {
     175            0 :             if (bottomOverlap > 0) {
     176              :                 // Resize the popup so that it doesn't extend beyond the bottom of the screen...
     177            0 :                 setContentHeight(m_desiredHeight - bottomOverlap);
     178              :             }
     179            0 :             return; // We're searching, so don't reposition the popup...
     180              :         }
     181              : 
     182            0 :         qreal desiredY = parentTopLeft.y() - viewRect.height();
     183            0 :         const qreal topOverlap = paddedAnchorRect.top() - desiredY;
     184            0 :         if (isSearchingAbove && topOverlap > 0) {
     185            0 :             setContentHeight(m_desiredHeight - topOverlap);
     186            0 :             viewRect.setHeight(viewRect.height() - topOverlap);
     187              : 
     188            0 :             desiredY = parentTopLeft.y() - viewRect.height(); // height changed - recompute...
     189              :             // No early return - we still need to actively position above...
     190              :         }
     191              : 
     192              :         // Searching right is an intermediate state that should never actually happen. When the popup is initially
     193              :         // positioned to the right and we start a search, we immediately reposition above...
     194            0 :         if (isSearchingAbove || isSearchingRight || paddedAnchorRect.top() < desiredY) {
     195              :             // Place above...
     196            0 :             movePos(m_globalPos.x(), desiredY);
     197            0 :             newPopupPos = PopupPosition::Top;
     198            0 :             return;
     199              :         }
     200              : 
     201            0 :         if (!isSearching()) {
     202              :             // Place to the right...
     203            0 :             movePos(parentTopLeft.x() + parent->width(), m_globalPos.y() - bottomOverlap);
     204            0 :             clampTopIfNeed();
     205            0 :             newPopupPos = PopupPosition::Right;
     206              :         }
     207            0 :     };
     208              : 
     209              :     // When searching a menu - we need to actively preserve the position / resize...
     210            0 :     const bool overlapsBottom = viewRect.bottom() > paddedAnchorRect.bottom();
     211            0 :     if (isSearching() || overlapsBottom) {
     212            0 :         doRepositionResize();
     213              :     }
     214              : 
     215            0 :     const Qt::AlignmentFlag parentCascadeAlign = this->parentCascadeAlign(parentMenuContentItem);
     216            0 :     if (viewRect.right() > paddedAnchorRect.right() || parentCascadeAlign != Qt::AlignmentFlag::AlignRight) {
     217            0 :         if (isCascade) {
     218              :             // move to the right of the parent
     219            0 :             movePos(parentTopLeft.x() - viewRect.width() + padding() * 2, m_globalPos.y());
     220            0 :             setCascadeAlign(Qt::AlignmentFlag::AlignLeft);
     221            0 :             newPopupPos = PopupPosition::Right;
     222              :         } else {
     223              :             // move to the left to an area that doesn't fit
     224            0 :             movePos(m_globalPos.x() - (viewRect.right() - paddedAnchorRect.right()) + padding() * 2, m_globalPos.y());
     225              :         }
     226              :     }
     227              : 
     228              :     // remove padding for arrow
     229            0 :     setPopupPosition(newPopupPos);
     230            0 :     movePos(m_globalPos.x() - padding(), m_globalPos.y());
     231            0 :     updateContentPosition();
     232              : }
     233              : 
     234            0 : void MenuView::updateContentPosition()
     235              : {
     236            0 :     if (popupPosition() == PopupPosition::Top) {
     237            0 :         contentItem()->setY(padding());
     238              :     } else {
     239            0 :         contentItem()->setY(-padding());
     240              :     }
     241            0 : }
     242              : 
     243            0 : QRect MenuView::viewGeometry() const
     244              : {
     245            0 :     return QRect(m_globalPos.toPoint(), QSize(contentWidth() + padding() * 2, contentHeight() + padding() * 2));
     246              : }
     247              : 
     248            0 : Qt::AlignmentFlag MenuView::parentCascadeAlign(const QQuickItem* parent) const
     249              : {
     250            0 :     if (!parent) {
     251            0 :         return Qt::AlignmentFlag::AlignRight;
     252              :     }
     253              : 
     254            0 :     return static_cast<Qt::AlignmentFlag>(parent->property("cascadeAlign").toInt());
     255              : }
     256              : 
     257            0 : QQuickItem* MenuView::parentMenuContentItem() const
     258              : {
     259            0 :     QQuickItem* parent = parentItem();
     260            0 :     while (parent) {
     261            0 :         if (parent->objectName().contains(MENU_VIEW_CONTENT_OBJECT_NAME)) {
     262            0 :             return parent;
     263              :         }
     264              : 
     265            0 :         parent = parent->parentItem();
     266              :     }
     267              : 
     268            0 :     return nullptr;
     269              : }
     270              : 
     271            0 : int MenuView::contentWidth() const
     272              : {
     273            0 :     return m_contentWidth;
     274              : }
     275              : 
     276            0 : void MenuView::setContentWidth(int newContentWidth)
     277              : {
     278            0 :     if (m_contentWidth == newContentWidth) {
     279            0 :         return;
     280              :     }
     281              : 
     282            0 :     m_contentWidth = newContentWidth;
     283            0 :     emit contentWidthChanged();
     284              : }
     285              : 
     286            0 : int MenuView::contentHeight() const
     287              : {
     288            0 :     return m_contentHeight;
     289              : }
     290              : 
     291            0 : void MenuView::setContentHeight(int newContentHeight)
     292              : {
     293            0 :     if (m_contentHeight == newContentHeight) {
     294            0 :         return;
     295              :     }
     296              : 
     297            0 :     m_contentHeight = newContentHeight;
     298            0 :     emit contentHeightChanged();
     299              : }
     300              : 
     301            0 : int MenuView::desiredHeight() const
     302              : {
     303            0 :     return m_desiredHeight;
     304              : }
     305              : 
     306            0 : void MenuView::setDesiredHeight(int desiredHeight)
     307              : {
     308            0 :     if (m_desiredHeight == desiredHeight) {
     309            0 :         return;
     310              :     }
     311              : 
     312            0 :     m_desiredHeight = desiredHeight;
     313            0 :     emit desiredHeightChanged();
     314              : 
     315            0 :     QMetaObject::invokeMethod(this, [this] {
     316            0 :         repositionWindowIfNeed();
     317            0 :     }, Qt::QueuedConnection);
     318              : }
     319              : 
     320            0 : int MenuView::desiredWidth() const
     321              : {
     322            0 :     return m_desiredWidth;
     323              : }
     324              : 
     325            0 : void MenuView::setDesiredWidth(int desiredWidth)
     326              : {
     327            0 :     if (m_desiredWidth == desiredWidth) {
     328            0 :         return;
     329              :     }
     330              : 
     331            0 :     m_desiredWidth = desiredWidth;
     332            0 :     emit desiredWidthChanged();
     333              : 
     334            0 :     QMetaObject::invokeMethod(this, [this] {
     335            0 :         repositionWindowIfNeed();
     336            0 :     }, Qt::QueuedConnection);
     337              : }
        

Generated by: LCOV version 2.0-1