LCOV - code coverage report
Current view: top level - src/appshell/qml/MuseScore/AppShell - navigableappmenumodel.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 0.0 % 287 0
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-Studio-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              : #include "navigableappmenumodel.h"
      23              : 
      24              : #include <QGuiApplication>
      25              : #include <QWindow>
      26              : #include <QKeyEvent>
      27              : 
      28              : #include <private/qkeymapper_p.h>
      29              : 
      30              : #include "ui/navigationcommands.h"
      31              : 
      32              : #include "log.h"
      33              : 
      34              : using namespace mu::appshell;
      35              : using namespace muse::ui;
      36              : using namespace muse::uicomponents;
      37              : 
      38            0 : QSet<int> convertToSet(QList<int> keys)
      39              : {
      40            0 :     return QSet<int>(keys.cbegin(), keys.cend());
      41              : }
      42              : 
      43            0 : QSet<int> convertToSet(QList<QKeyCombination> keys)
      44              : {
      45            0 :     QSet<int> keyset;
      46            0 :     for (const auto& key : keys) {
      47            0 :         keyset << key.toCombined();
      48              :     }
      49            0 :     return keyset;
      50            0 : }
      51              : 
      52            0 : QSet<int> possibleKeys(QKeyEvent* keyEvent)
      53              : {
      54            0 :     QKeyEvent* correctedKeyEvent = keyEvent;
      55              :     //! NOTE: correct work only with alt modifier
      56            0 :     correctedKeyEvent->setModifiers(Qt::AltModifier);
      57              : 
      58            0 :     auto keys = QKeyMapper::possibleKeys(correctedKeyEvent);
      59            0 :     return convertToSet(keys);
      60            0 : }
      61              : 
      62            0 : QSet<int> possibleKeys(const QChar& keySymbol)
      63              : {
      64            0 :     QKeyEvent fakeKey(QKeyEvent::KeyRelease, Qt::Key_unknown, Qt::AltModifier, keySymbol);
      65            0 :     auto keys = QKeyMapper::possibleKeys(&fakeKey);
      66              : 
      67            0 :     return convertToSet(keys);
      68            0 : }
      69              : 
      70            0 : NavigableAppMenuModel::NavigableAppMenuModel(QObject* parent)
      71            0 :     : AppMenuModel(parent)
      72              : {
      73            0 : }
      74              : 
      75            0 : void NavigableAppMenuModel::load()
      76              : {
      77            0 :     AppMenuModel::load();
      78              : 
      79            0 :     connect(qApp, &QGuiApplication::applicationStateChanged, this, [this](Qt::ApplicationState state){
      80            0 :         if (state != Qt::ApplicationActive) {
      81            0 :             resetNavigation();
      82              :         }
      83            0 :     });
      84              : 
      85            0 :     navigationController()->navigationChanged().onNotify(this, [this](){
      86            0 :         if (navigationController()->isHighlight() && !isMenuOpened()) {
      87            0 :             resetNavigation();
      88              :         }
      89            0 :     });
      90              : 
      91            0 :     qApp->installEventFilter(this);
      92            0 : }
      93              : 
      94            0 : void NavigableAppMenuModel::handleMenuItem(const QString& itemId)
      95              : {
      96            0 :     resetNavigation();
      97            0 :     restoreMUNavigationSystemState();
      98              : 
      99            0 :     AppMenuModel::handleMenuItem(itemId);
     100            0 : }
     101              : 
     102            0 : void NavigableAppMenuModel::openPrevMenu()
     103              : {
     104            0 :     navigate(Qt::Key_Left);
     105            0 :     activateHighlightedMenu();
     106            0 : }
     107              : 
     108            0 : void NavigableAppMenuModel::openNextMenu()
     109              : {
     110            0 :     navigate(Qt::Key_Right);
     111            0 :     activateHighlightedMenu();
     112            0 : }
     113              : 
     114            0 : void NavigableAppMenuModel::openMenu(const QString& menuId, bool byHover)
     115              : {
     116            0 :     bool navigationStarted = isNavigationStarted();
     117            0 :     bool menuIsAlreadyOpened = m_openedMenuId == menuId;
     118            0 :     if (!byHover && !menuIsAlreadyOpened) {
     119            0 :         if (navigationStarted || !isMenuOpened()) {
     120            0 :             saveMUNavigationSystemState();
     121              :         } else {
     122            0 :             restoreMUNavigationSystemState();
     123              :         }
     124              :     }
     125              : 
     126            0 :     if (navigationStarted) {
     127            0 :         setHighlightedMenuId(menuId);
     128              :     }
     129              : 
     130            0 :     emit openMenuRequested(menuId, byHover);
     131            0 : }
     132              : 
     133            0 : bool NavigableAppMenuModel::isNavigationStarted() const
     134              : {
     135            0 :     return !m_highlightedMenuId.isEmpty();
     136              : }
     137              : 
     138            0 : bool NavigableAppMenuModel::isMenuOpened() const
     139              : {
     140            0 :     return !m_openedMenuId.isEmpty();
     141              : }
     142              : 
     143            0 : QWindow* NavigableAppMenuModel::appWindow() const
     144              : {
     145            0 :     return m_appWindow;
     146              : }
     147              : 
     148            0 : void NavigableAppMenuModel::setAppWindow(QWindow* appWindow)
     149              : {
     150            0 :     m_appWindow = appWindow;
     151            0 : }
     152              : 
     153            0 : void NavigableAppMenuModel::setHighlightedMenuId(QString highlightedMenuId)
     154              : {
     155            0 :     if (m_highlightedMenuId == highlightedMenuId) {
     156            0 :         return;
     157              :     }
     158              : 
     159            0 :     m_highlightedMenuId = highlightedMenuId;
     160            0 :     emit highlightedMenuIdChanged(m_highlightedMenuId);
     161              : }
     162              : 
     163            0 : void NavigableAppMenuModel::setOpenedMenuId(QString openedMenuId)
     164              : {
     165            0 :     if (m_openedMenuId == openedMenuId) {
     166            0 :         return;
     167              :     }
     168              : 
     169            0 :     m_openedMenuId = openedMenuId;
     170              : 
     171            0 :     bool navigationStarted = isNavigationStarted();
     172            0 :     bool menuOpened = isMenuOpened();
     173              : 
     174              :     //! NOTE: When the user navigates through the menu, MU navigation is highlighted.
     175              :     //!       Reset the highlighted state after the menu is closed
     176            0 :     if (navigationStarted) {
     177            0 :         navigationController()->setIsHighlight(false);
     178              :     }
     179              : 
     180              :     //! NOTE: user closed menu by mouse
     181            0 :     if (!navigationStarted && !menuOpened) {
     182            0 :         restoreMUNavigationSystemState();
     183              :     }
     184              : 
     185              :     //! NOTE: after opening the menu, position on the first control
     186            0 :     if (navigationStarted && menuOpened) {
     187            0 :         commandDispatcher()->dispatch(FIRST_CONTROL_COMMAND);
     188              :     }
     189              : 
     190            0 :     emit openedMenuIdChanged(m_openedMenuId);
     191              : }
     192              : 
     193            0 : void NavigableAppMenuModel::setAppMenuAreaRect(QRect appMenuAreaRect)
     194              : {
     195            0 :     if (m_appMenuAreaRect == appMenuAreaRect) {
     196            0 :         return;
     197              :     }
     198              : 
     199            0 :     m_appMenuAreaRect = appMenuAreaRect;
     200            0 :     emit appMenuAreaRectChanged(m_appMenuAreaRect);
     201              : }
     202              : 
     203            0 : void NavigableAppMenuModel::setOpenedMenuAreaRect(QRect openedMenuAreaRect)
     204              : {
     205            0 :     if (m_openedMenuAreaRect == openedMenuAreaRect) {
     206            0 :         return;
     207              :     }
     208              : 
     209            0 :     m_openedMenuAreaRect = openedMenuAreaRect;
     210            0 :     emit openedMenuAreaRectChanged(m_openedMenuAreaRect);
     211              : }
     212              : 
     213            0 : bool NavigableAppMenuModel::eventFilter(QObject* watched, QEvent* event)
     214              : {
     215            0 :     bool isMenuOpened = !m_openedMenuId.isEmpty();
     216            0 :     if (event->type() == QEvent::MouseButtonPress && watched == appWindow()) {
     217            0 :         QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
     218            0 :         bool clickOutsideAppMenuAreaRect = !m_appMenuAreaRect.contains(mouseEvent->pos());
     219            0 :         bool clickOutsideOpenedMenu = isMenuOpened ? !m_openedMenuAreaRect.contains(mouseEvent->pos()) : true;
     220            0 :         if (clickOutsideAppMenuAreaRect && clickOutsideOpenedMenu) {
     221            0 :             resetNavigation();
     222            0 :             emit closeOpenedMenuRequested();
     223            0 :             return false;
     224              :         }
     225              :     }
     226              : 
     227            0 :     if (isMenuOpened && watched && watched->isWindowType()) {
     228            0 :         return processEventForOpenedMenu(event);
     229              :     }
     230              : 
     231            0 :     if (watched == appWindow()) {
     232            0 :         bool ok = processEventForAppMenu(event);
     233            0 :         if (ok) {
     234            0 :             return ok;
     235              :         }
     236              :     }
     237              : 
     238            0 :     return AbstractMenuModel::eventFilter(watched, event);
     239              : }
     240              : 
     241            0 : bool NavigableAppMenuModel::processEventForOpenedMenu(QEvent* event)
     242              : {
     243            0 :     if (event->type() != QEvent::ShortcutOverride) {
     244            0 :         return false;
     245              :     }
     246              : 
     247            0 :     QKeyEvent* keyEvent = dynamic_cast<QKeyEvent*>(event);
     248              : 
     249            0 :     bool isNavigationWithSymbol = !keyEvent->modifiers()
     250            0 :                                   && keyEvent->text().length() == 1;
     251              : 
     252            0 :     if (!isNavigationWithSymbol || isNavigateKey(keyEvent->key())) {
     253            0 :         return false;
     254              :     }
     255              : 
     256            0 :     QChar symbol = keyEvent->text()[0];
     257            0 :     emit navigateWithSymbolRequested(symbol);
     258            0 :     event->accept();
     259            0 :     return true;
     260              : }
     261              : 
     262            0 : bool NavigableAppMenuModel::processEventForAppMenu(QEvent* event)
     263              : {
     264            0 :     QKeyEvent* keyEvent = dynamic_cast<QKeyEvent*>(event);
     265            0 :     if (!keyEvent) {
     266            0 :         return false;
     267              :     }
     268              : 
     269            0 :     Qt::KeyboardModifiers modifiers = keyEvent->modifiers();
     270            0 :     int key = keyEvent->key();
     271            0 :     bool isSingleSymbol = keyEvent->text().length() == 1;
     272              : 
     273            0 :     bool isNavigationStarted = this->isNavigationStarted();
     274            0 :     bool isNavigationWithSymbol = !modifiers
     275            0 :                                   && isSingleSymbol
     276            0 :                                   && isNavigationStarted;
     277            0 :     bool isNavigationWithAlt = (modifiers == Qt::AltModifier)
     278            0 :                                && isSingleSymbol;
     279              : 
     280              :     bool isAltKey = key == Qt::Key_Alt
     281            0 :                     && !(modifiers & Qt::ControlModifier)
     282            0 :                     && !(modifiers & Qt::ShiftModifier);
     283              : 
     284            0 :     switch (event->type()) {
     285            0 :     case QEvent::ShortcutOverride: {
     286            0 :         if (isNavigationStarted && isNavigateKey(key)) {
     287            0 :             event->accept();
     288            0 :             return true;
     289            0 :         } else if (isNavigationWithSymbol || isNavigationWithAlt) {
     290            0 :             QSet<int> activatePossibleKeys = possibleKeys(keyEvent);
     291            0 :             if (hasItem(activatePossibleKeys)) {
     292            0 :                 event->accept();
     293            0 :                 return true;
     294              :             }
     295            0 :         }
     296              : 
     297            0 :         m_needActivateHighlight = false;
     298              : 
     299            0 :         break;
     300              :     }
     301            0 :     case QEvent::KeyPress: {
     302            0 :         if (isAltKey) {
     303            0 :             m_needActivateHighlight = true;
     304            0 :             break;
     305              :         }
     306              : 
     307            0 :         m_needActivateHighlight = false;
     308              : 
     309            0 :         if (isNavigationStarted && isNavigateKey(key)) {
     310            0 :             navigate(key);
     311              : 
     312            0 :             event->accept();
     313            0 :             return true;
     314            0 :         } else if (isNavigationWithSymbol || isNavigationWithAlt) {
     315            0 :             QSet<int> activatePossibleKeys = possibleKeys(keyEvent);
     316            0 :             if (hasItem(activatePossibleKeys)) {
     317            0 :                 navigate(activatePossibleKeys);
     318              : 
     319            0 :                 event->accept();
     320            0 :                 return true;
     321              :             }
     322            0 :         }
     323              : 
     324            0 :         break;
     325              :     }
     326            0 :     case QEvent::KeyRelease: {
     327            0 :         if (!isAltKey) {
     328            0 :             break;
     329              :         }
     330              : 
     331            0 :         if (isNavigationStarted) {
     332            0 :             resetNavigation();
     333            0 :             restoreMUNavigationSystemState();
     334              :         } else {
     335            0 :             if (m_needActivateHighlight) {
     336            0 :                 m_needActivateHighlight = false;
     337            0 :                 saveMUNavigationSystemState();
     338            0 :                 navigateToFirstMenu();
     339              :             }
     340              :         }
     341              : 
     342            0 :         event->accept();
     343            0 :         return true;
     344              :     }
     345            0 :     case QEvent::MouseButtonPress: {
     346            0 :         resetNavigation();
     347            0 :         break;
     348              :     }
     349            0 :     default:
     350            0 :         break;
     351              :     }
     352            0 :     return false;
     353              : }
     354              : 
     355            0 : bool NavigableAppMenuModel::isNavigateKey(int key) const
     356              : {
     357              :     static const QList<Qt::Key> keys {
     358              :         Qt::Key_Left,
     359              :         Qt::Key_Right,
     360              :         Qt::Key_Down,
     361              :         Qt::Key_Space,
     362              :         Qt::Key_Escape,
     363              :         Qt::Key_Return,
     364              :         Qt::Key_Enter
     365            0 :     };
     366              : 
     367            0 :     return keys.contains(static_cast<Qt::Key>(key));
     368              : }
     369              : 
     370            0 : void NavigableAppMenuModel::navigate(int scanCode)
     371              : {
     372            0 :     Qt::Key _key = static_cast<Qt::Key>(scanCode);
     373            0 :     switch (_key) {
     374            0 :     case Qt::Key_Left: {
     375            0 :         int newIndex = itemIndex(m_highlightedMenuId) - 1;
     376            0 :         if (newIndex < 0) {
     377            0 :             newIndex = rowCount() - 1;
     378              :         }
     379              : 
     380            0 :         setHighlightedMenuId(item(newIndex).id());
     381            0 :         break;
     382              :     }
     383            0 :     case Qt::Key_Right: {
     384            0 :         int newIndex = itemIndex(m_highlightedMenuId) + 1;
     385            0 :         if (newIndex > rowCount() - 1) {
     386            0 :             newIndex = 0;
     387              :         }
     388              : 
     389            0 :         setHighlightedMenuId(item(newIndex).id());
     390            0 :         break;
     391              :     }
     392            0 :     case Qt::Key_Down:
     393              :     case Qt::Key_Space:
     394              :     case Qt::Key_Return:
     395              :     case Qt::Key_Enter:
     396            0 :         activateHighlightedMenu();
     397            0 :         break;
     398            0 :     case Qt::Key_Escape:
     399            0 :         resetNavigation();
     400            0 :         restoreMUNavigationSystemState();
     401            0 :         break;
     402            0 :     default:
     403            0 :         break;
     404              :     }
     405            0 : }
     406              : 
     407            0 : bool NavigableAppMenuModel::hasItem(const QSet<int>& activatePossibleKeys)
     408              : {
     409            0 :     return !menuItemId(items(), activatePossibleKeys).isEmpty();
     410              : }
     411              : 
     412            0 : void NavigableAppMenuModel::navigate(const QSet<int>& activatePossibleKeys)
     413              : {
     414            0 :     saveMUNavigationSystemState();
     415              : 
     416            0 :     setHighlightedMenuId(menuItemId(items(), activatePossibleKeys));
     417            0 :     activateHighlightedMenu();
     418            0 : }
     419              : 
     420            0 : void NavigableAppMenuModel::resetNavigation()
     421              : {
     422            0 :     setHighlightedMenuId("");
     423            0 :     m_needActivateHighlight = false;
     424            0 : }
     425              : 
     426            0 : void NavigableAppMenuModel::navigateToFirstMenu()
     427              : {
     428            0 :     setHighlightedMenuId(item(0).id());
     429            0 : }
     430              : 
     431            0 : QRect NavigableAppMenuModel::appMenuAreaRect() const
     432              : {
     433            0 :     return m_appMenuAreaRect;
     434              : }
     435              : 
     436            0 : QRect NavigableAppMenuModel::openedMenuAreaRect() const
     437              : {
     438            0 :     return m_openedMenuAreaRect;
     439              : }
     440              : 
     441            0 : void NavigableAppMenuModel::saveMUNavigationSystemState()
     442              : {
     443            0 :     if (m_lastActiveMUNavigationState.has_value()) {
     444            0 :         return;
     445              :     }
     446              : 
     447            0 :     bool muNavigationIsHighlight = navigationController()->isHighlight();
     448            0 :     m_needActivateLastMUNavigationControl = muNavigationIsHighlight;
     449              : 
     450            0 :     INavigationSection* section = navigationController()->activeSection();
     451            0 :     INavigationPanel* panel = navigationController()->activePanel();
     452            0 :     INavigationControl* control = navigationController()->activeControl();
     453              :     m_lastActiveMUNavigationState = {
     454            0 :         section ? section->name().toStdString() : "",
     455            0 :         panel ? panel->name().toStdString() : "",
     456            0 :         control ? control->name().toStdString() : ""
     457            0 :     };
     458              : 
     459            0 :     if (control) {
     460            0 :         control->setActive(false);
     461              :     }
     462            0 : }
     463              : 
     464            0 : void NavigableAppMenuModel::restoreMUNavigationSystemState()
     465              : {
     466            0 :     if (m_lastActiveMUNavigationState.has_value()) {
     467            0 :         MUNavigationSystemState state = m_lastActiveMUNavigationState.value();
     468              : 
     469            0 :         bool ok = navigationController()->requestActivateByName(state.sectionName, state.panelName, state.controlName);
     470            0 :         if (!ok) {
     471            0 :             navigationController()->resetNavigation();
     472              :         }
     473              : 
     474            0 :         m_lastActiveMUNavigationState.reset();
     475            0 :     }
     476              : 
     477            0 :     navigationController()->setIsHighlight(m_needActivateLastMUNavigationControl);
     478            0 : }
     479              : 
     480            0 : void NavigableAppMenuModel::activateHighlightedMenu()
     481              : {
     482            0 :     emit openMenuRequested(m_highlightedMenuId, false);
     483            0 : }
     484              : 
     485            0 : QString NavigableAppMenuModel::highlightedMenuId() const
     486              : {
     487            0 :     return m_highlightedMenuId;
     488              : }
     489              : 
     490            0 : QString NavigableAppMenuModel::openedMenuId() const
     491              : {
     492            0 :     return m_openedMenuId;
     493              : }
     494              : 
     495            0 : bool NavigableAppMenuModel::menuItemMatchesSymbol(MenuItem* menuItem, const QChar& symbol)
     496              : {
     497            0 :     QString title = menuItem->titleWithMnemonicAmpersand();
     498              : 
     499            0 :     int activateKeyIndex = title.indexOf('&');
     500            0 :     if (activateKeyIndex == -1) {
     501            0 :         return false;
     502              :     }
     503              : 
     504            0 :     auto keys = possibleKeys(symbol.toUpper());
     505            0 :     auto menuActivatePossibleKeys = possibleKeys(title[activateKeyIndex + 1].toUpper());
     506            0 :     if (menuActivatePossibleKeys.intersects(keys)) {
     507            0 :         return true;
     508              :     }
     509              : 
     510            0 :     return false;
     511            0 : }
     512              : 
     513            0 : QString NavigableAppMenuModel::menuItemId(const MenuItemList& items, const QSet<int>& activatePossibleKeys)
     514              : {
     515            0 :     for (const MenuItem* item : items) {
     516            0 :         QString title = item->titleWithMnemonicAmpersand();
     517              : 
     518            0 :         int activateKeyIndex = title.indexOf('&');
     519            0 :         if (activateKeyIndex == -1) {
     520            0 :             continue;
     521              :         }
     522              : 
     523            0 :         auto menuActivatePossibleKeys = possibleKeys(title[activateKeyIndex + 1].toUpper());
     524            0 :         if (menuActivatePossibleKeys.intersects(activatePossibleKeys)) {
     525            0 :             return item->id();
     526              :         }
     527            0 :     }
     528              : 
     529            0 :     return QString();
     530              : }
        

Generated by: LCOV version 2.0-1