LCOV - code coverage report
Current view: top level - muse/framework/ui/qml/Muse/Ui - initialletternavigation.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 0.0 % 96 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) 2025 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 "initialletternavigation.h"
      24              : 
      25              : #include <QRegularExpression>
      26              : 
      27              : #include "global/log.h"
      28              : 
      29              : #include "navigationpanel.h"
      30              : 
      31              : using namespace muse::ui;
      32              : 
      33              : static QSet<Qt::Key> NAVIGATION_KEYS = {
      34              :     Qt::Key_Enter,
      35              :     Qt::Key_Return,
      36              :     Qt::Key_Up,
      37              :     Qt::Key_Down,
      38              :     Qt::Key_Left,
      39              :     Qt::Key_Right,
      40              :     Qt::Key_Tab,
      41              :     Qt::Key_PageUp,
      42              :     Qt::Key_PageDown,
      43              : };
      44              : 
      45            0 : static QString normalizeForSearch(const QString& string)
      46              : {
      47            0 :     return string.normalized(QString::NormalizationForm_KD).toLower();
      48              : }
      49              : 
      50            0 : InitialLetterNavigation::InitialLetterNavigation(QObject* parent)
      51            0 :     : QObject(parent), muse::Contextable(muse::iocCtxForQmlObject(this))
      52              : {
      53            0 :     m_inputBufferTimer.setSingleShot(true);
      54            0 :     m_inputBufferTimer.setInterval(1000);
      55              : 
      56            0 :     QObject::connect(&m_inputBufferTimer, &QTimer::timeout, this, [this]() {
      57            0 :         clearBuffer();
      58            0 :     });
      59              : 
      60            0 :     qApp->installEventFilter(this);
      61            0 : }
      62              : 
      63            0 : QStringList InitialLetterNavigation::stringList() const
      64              : {
      65            0 :     return m_stringList;
      66              : }
      67              : 
      68            0 : void InitialLetterNavigation::setStringList(const QStringList& stringList)
      69              : {
      70            0 :     if (stringList == m_stringList) {
      71            0 :         return;
      72              :     }
      73            0 :     m_stringList = stringList;
      74            0 :     emit stringListChanged();
      75              : }
      76              : 
      77            0 : NavigationPanel* InitialLetterNavigation::panel() const
      78              : {
      79            0 :     return m_panel;
      80              : }
      81              : 
      82            0 : void InitialLetterNavigation::setPanel(NavigationPanel* panel)
      83              : {
      84            0 :     if (panel == m_panel) {
      85            0 :         return;
      86              :     }
      87            0 :     m_panel = panel;
      88            0 :     emit panelChanged();
      89              : }
      90              : 
      91            0 : int InitialLetterNavigation::controlColumn() const
      92              : {
      93            0 :     return m_controlColumn;
      94              : }
      95              : 
      96            0 : void InitialLetterNavigation::setControlColumn(int controlColumn)
      97              : {
      98            0 :     if (controlColumn == m_controlColumn) {
      99            0 :         return;
     100              :     }
     101            0 :     m_controlColumn = controlColumn;
     102            0 :     emit controlColumnChanged();
     103              : }
     104              : 
     105            0 : bool InitialLetterNavigation::eventFilter(QObject* watched, QEvent* event)
     106              : {
     107            0 :     if (!event || event->type() != QEvent::ShortcutOverride) {
     108            0 :         return QObject::eventFilter(watched, event);
     109              :     }
     110              : 
     111            0 :     const QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
     112              : 
     113            0 :     const Qt::Key key = static_cast<Qt::Key>(keyEvent->key());
     114            0 :     const bool eventKeyIsSpace = key == Qt::Key::Key_Space;
     115            0 :     const bool isNavKey = NAVIGATION_KEYS.contains(key);
     116            0 :     if (isNavKey || (eventKeyIsSpace && m_inputString.isEmpty())) {
     117              :         // Ignore all navigation keys apart from space (unless space is the first thing we type)...
     118            0 :         clearBuffer();
     119            0 :         return QObject::eventFilter(watched, event);
     120              :     }
     121              : 
     122            0 :     const QString eventText = normalizeForSearch(keyEvent->text());
     123              : 
     124            0 :     static const QRegularExpression controlCharsExceptSpace("[\\x00-\\x1F\\x7F]");
     125            0 :     if (eventText.isEmpty() || eventText.contains(controlCharsExceptSpace)) {
     126              :         // Ignore text containing ASCII control characters...
     127            0 :         clearBuffer();
     128            0 :         return QObject::eventFilter(watched, event);
     129              :     }
     130              : 
     131            0 :     event->accept();
     132            0 :     m_inputBufferTimer.start();
     133              : 
     134              :     // If the last input was the same as this one we "cycle" the entries starting at the active control...
     135            0 :     if (!m_prevEventText.isEmpty() && eventText != m_prevEventText) {
     136            0 :         m_cycling = false; // Keep it `true` on the first keypress.
     137              :     }
     138            0 :     m_inputString.append(eventText);
     139            0 :     m_prevEventText = eventText;
     140              : 
     141            0 :     const int indexOfMatch = indexForInput(eventText);
     142            0 :     if (indexOfMatch < 0) {
     143            0 :         return !eventKeyIsSpace; // Ignore space if it's the last character typed and there are no matches...
     144              :     }
     145              : 
     146              :     //! NOTE: This has to happen before navigateToIndex in some cases - the view might be
     147              :     //! deleting navigation controls when they're not visible...
     148            0 :     emit requestVisible(indexOfMatch);
     149              : 
     150            0 :     const INavigation::Index navIndex = { m_controlColumn, indexOfMatch };
     151            0 :     navigateToIndex(navIndex);
     152              : 
     153            0 :     return true;
     154            0 : }
     155              : 
     156            0 : int InitialLetterNavigation::indexForInput(const QString& eventText) const
     157              : {
     158            0 :     const INavigationControl* control = navigationController()->activeControl();
     159            0 :     const int activeIndex = control ? control->index().row : 0;
     160            0 :     const int startIndex = activeIndex + (m_cycling ? 1 : 0);
     161              : 
     162            0 :     const auto indexHasMatch = [this, eventText](int index) -> bool {
     163            0 :         const QString& itemString = normalizeForSearch(m_stringList.at(index));
     164            0 :         const QString& searchString = m_cycling ? eventText : m_inputString;
     165            0 :         return itemString.startsWith(searchString);
     166            0 :     };
     167              : 
     168            0 :     for (int i = startIndex; i < m_stringList.size(); ++i) {
     169            0 :         if (indexHasMatch(i)) {
     170            0 :             return i;
     171              :         }
     172              :     }
     173              : 
     174            0 :     for (int i = 0; i < startIndex; ++i) {
     175            0 :         if (indexHasMatch(i)) {
     176            0 :             return i;
     177              :         }
     178              :     }
     179              : 
     180            0 :     return -1;
     181            0 : }
     182              : 
     183            0 : void InitialLetterNavigation::navigateToIndex(const INavigation::Index& index)
     184              : {
     185            0 :     const INavigationSection* section = m_panel ? m_panel->section() : nullptr;
     186            0 :     IF_ASSERT_FAILED(section) {
     187            0 :         return;
     188              :     }
     189            0 :     const std::string sectionName = section->name().toStdString();
     190            0 :     const std::string panelName = m_panel->name().toStdString();
     191            0 :     const bool ok = navigationController()->requestActivateByIndex(sectionName, panelName, index);
     192            0 :     IF_ASSERT_FAILED(ok) {
     193            0 :         LOGE() << "Index not found in this panel/section - make sure nav control indices match m_stringList indices";
     194            0 :         return;
     195              :     }
     196              : 
     197            0 :     if (!navigationController()->isHighlight()) {
     198            0 :         navigationController()->setIsHighlight(true);
     199              :     }
     200            0 : }
     201              : 
     202            0 : void InitialLetterNavigation::clearBuffer()
     203              : {
     204            0 :     m_cycling = true;
     205            0 :     m_inputString.clear();
     206            0 :     m_prevEventText.clear();
     207            0 : }
        

Generated by: LCOV version 2.0-1