LCOV - code coverage report
Current view: top level - muse/framework/uicomponents/qml/Muse/UiComponents - sectionproxymodel.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 85.9 % 249 214
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) 2026 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 "sectionproxymodel.h"
      24              : 
      25              : #include "sortfilterproxymodel.h"
      26              : 
      27              : using namespace muse::uicomponents;
      28              : 
      29            9 : SectionProxyModel::SectionProxyModel(QObject* parent)
      30            9 :     : QAbstractProxyModel(parent)
      31              : {
      32            9 : }
      33              : 
      34            0 : QString SectionProxyModel::sectionRoleName() const
      35              : {
      36            0 :     return m_sectionRoleName;
      37              : }
      38              : 
      39           10 : void SectionProxyModel::setSectionRoleName(const QString& roleName)
      40              : {
      41           10 :     if (m_sectionRoleName == roleName) {
      42            0 :         return;
      43              :     }
      44              : 
      45           10 :     m_sectionRoleName = roleName;
      46           10 :     rebuild();
      47              : 
      48           10 :     emit sectionRoleNameChanged();
      49              : }
      50              : 
      51            1 : QStringList SectionProxyModel::collapsedSections() const
      52              : {
      53            1 :     return m_collapsedSections;
      54              : }
      55              : 
      56            2 : void SectionProxyModel::setCollapsedSections(const QStringList& sections)
      57              : {
      58            2 :     if (m_collapsedSections == sections) {
      59            0 :         return;
      60              :     }
      61              : 
      62            2 :     m_collapsedSections = sections;
      63            2 :     rebuild();
      64              : 
      65            2 :     emit collapsedSectionsChanged();
      66              : }
      67              : 
      68            1 : void SectionProxyModel::toggleSection(const QString& section)
      69              : {
      70            1 :     setSectionCollapsed(section, !isCollapsed(section));
      71            1 : }
      72              : 
      73            4 : void SectionProxyModel::setSectionCollapsed(const QString& section, bool collapsed)
      74              : {
      75            4 :     if (isCollapsed(section) == collapsed) {
      76            0 :         return;
      77              :     }
      78              : 
      79            4 :     const int headerRow = sectionRow(section);
      80              : 
      81            4 :     if (collapsed) {
      82            2 :         m_collapsedSections.append(section);
      83              : 
      84            2 :         if (headerRow >= 0) {
      85            2 :             int lastRow = headerRow;
      86            6 :             while (lastRow + 1 < m_rows.size() && !m_rows.at(lastRow + 1).isSection) {
      87            4 :                 ++lastRow;
      88              :             }
      89              : 
      90            2 :             if (lastRow > headerRow) {
      91            2 :                 beginRemoveRows(QModelIndex(), headerRow + 1, lastRow);
      92            2 :                 m_rows.remove(headerRow + 1, lastRow - headerRow);
      93            2 :                 updateRowIndexes();
      94            2 :                 endRemoveRows();
      95              : 
      96            2 :                 emit rowCountChanged();
      97              :             }
      98              :         }
      99              :     } else {
     100            2 :         m_collapsedSections.removeAll(section);
     101              : 
     102            2 :         const int roleId = sectionRoleId();
     103            2 :         if (headerRow >= 0 && roleId != INVALID_ROLE_ID && sourceModel()) {
     104            2 :             QList<Row> newRows;
     105            2 :             const int sourceRowCount = sourceModel()->rowCount();
     106              : 
     107           12 :             for (int i = 0; i < sourceRowCount; ++i) {
     108           10 :                 if (sectionOfSourceRow(i, roleId) != section) {
     109            6 :                     continue;
     110              :                 }
     111              : 
     112            4 :                 Row row;
     113            4 :                 row.section = section;
     114            4 :                 row.sourceRow = i;
     115            4 :                 newRows.append(row);
     116            4 :             }
     117              : 
     118            2 :             if (!newRows.isEmpty()) {
     119            2 :                 beginInsertRows(QModelIndex(), headerRow + 1, headerRow + newRows.size());
     120            6 :                 for (int i = 0; i < newRows.size(); ++i) {
     121            4 :                     m_rows.insert(headerRow + 1 + i, newRows.at(i));
     122              :                 }
     123            2 :                 updateRowIndexes();
     124            2 :                 endInsertRows();
     125              : 
     126            2 :                 emit rowCountChanged();
     127              :             }
     128            2 :         }
     129              :     }
     130              : 
     131            4 :     if (headerRow >= 0) {
     132            4 :         const QModelIndex idx = index(headerRow, 0);
     133            4 :         emit dataChanged(idx, idx, { RoleSectionExpanded });
     134              :     }
     135              : 
     136            4 :     emit collapsedSectionsChanged();
     137              : }
     138              : 
     139            1 : void SectionProxyModel::expandAll()
     140              : {
     141            1 :     setCollapsedSections(QStringList());
     142            1 : }
     143              : 
     144            1 : void SectionProxyModel::collapseAll()
     145              : {
     146            1 :     QStringList sections;
     147            9 :     for (const Row& row : std::as_const(m_rows)) {
     148            8 :         if (row.isSection) {
     149            3 :             sections.append(row.section);
     150              :         }
     151              :     }
     152              : 
     153            1 :     setCollapsedSections(sections);
     154            1 : }
     155              : 
     156            1 : bool SectionProxyModel::isSectionRow(int row) const
     157              : {
     158            1 :     if (row < 0 || row >= m_rows.size()) {
     159            0 :         return false;
     160              :     }
     161              : 
     162            1 :     return m_rows.at(row).isSection;
     163              : }
     164              : 
     165           30 : int SectionProxyModel::sourceRowOf(int row) const
     166              : {
     167           30 :     if (row < 0 || row >= m_rows.size()) {
     168            0 :         return -1;
     169              :     }
     170              : 
     171           30 :     return m_rows.at(row).sourceRow;
     172              : }
     173              : 
     174            9 : void SectionProxyModel::setSourceModel(QAbstractItemModel* sourceModel)
     175              : {
     176            9 :     for (const QMetaObject::Connection& connection : std::as_const(m_sourceConnections)) {
     177            0 :         disconnect(connection);
     178              :     }
     179            9 :     m_sourceConnections.clear();
     180              : 
     181            9 :     QAbstractProxyModel::setSourceModel(sourceModel);
     182              : 
     183            9 :     if (sourceModel) {
     184            1 :         const auto rebuildOnChange = [this]() { rebuild(); };
     185              : 
     186            9 :         m_sourceConnections << connect(sourceModel, &QAbstractItemModel::modelReset, this, rebuildOnChange);
     187            9 :         m_sourceConnections << connect(sourceModel, &QAbstractItemModel::layoutChanged, this, rebuildOnChange);
     188            9 :         m_sourceConnections << connect(sourceModel, &QAbstractItemModel::rowsInserted, this, rebuildOnChange);
     189            9 :         m_sourceConnections << connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, rebuildOnChange);
     190            9 :         m_sourceConnections << connect(sourceModel, &QAbstractItemModel::rowsMoved, this, rebuildOnChange);
     191            9 :         m_sourceConnections << connect(sourceModel, &QAbstractItemModel::dataChanged, this,
     192            9 :                                        &SectionProxyModel::onSourceDataChanged);
     193              : 
     194            9 :         if (auto sortFilterModel = qobject_cast<SortFilterProxyModel*>(sourceModel)) {
     195            8 :             m_sourceConnections << connect(sortFilterModel, &SortFilterProxyModel::sourceModelRoleNamesChanged,
     196            8 :                                            this, rebuildOnChange);
     197              :         }
     198              :     }
     199              : 
     200            9 :     rebuild();
     201            9 : }
     202              : 
     203           24 : QModelIndex SectionProxyModel::mapToSource(const QModelIndex& proxyIndex) const
     204              : {
     205           24 :     if (!proxyIndex.isValid() || !sourceModel()) {
     206            0 :         return QModelIndex();
     207              :     }
     208              : 
     209           24 :     const int sourceRow = sourceRowOf(proxyIndex.row());
     210           24 :     if (sourceRow < 0) {
     211            1 :         return QModelIndex();
     212              :     }
     213              : 
     214           23 :     return sourceModel()->index(sourceRow, proxyIndex.column());
     215              : }
     216              : 
     217            4 : QModelIndex SectionProxyModel::mapFromSource(const QModelIndex& sourceIndex) const
     218              : {
     219            4 :     if (!sourceIndex.isValid()) {
     220            0 :         return QModelIndex();
     221              :     }
     222              : 
     223            4 :     const int proxyRow = m_sourceRowToProxyRow.value(sourceIndex.row(), -1);
     224            4 :     if (proxyRow < 0) {
     225            1 :         return QModelIndex();
     226              :     }
     227              : 
     228            3 :     return index(proxyRow, sourceIndex.column());
     229              : }
     230              : 
     231           70 : QModelIndex SectionProxyModel::index(int row, int column, const QModelIndex& parent) const
     232              : {
     233           70 :     if (parent.isValid() || row < 0 || row >= m_rows.size() || column < 0 || column >= columnCount()) {
     234            0 :         return QModelIndex();
     235              :     }
     236              : 
     237           70 :     return createIndex(row, column);
     238              : }
     239              : 
     240            0 : QModelIndex SectionProxyModel::parent(const QModelIndex&) const
     241              : {
     242            0 :     return QModelIndex();
     243              : }
     244              : 
     245           23 : int SectionProxyModel::rowCount(const QModelIndex& parent) const
     246              : {
     247           23 :     if (parent.isValid()) {
     248            0 :         return 0;
     249              :     }
     250              : 
     251           23 :     return m_rows.size();
     252              : }
     253              : 
     254           70 : int SectionProxyModel::columnCount(const QModelIndex& parent) const
     255              : {
     256           70 :     if (parent.isValid() || !sourceModel()) {
     257            0 :         return 0;
     258              :     }
     259              : 
     260           70 :     return sourceModel()->columnCount();
     261              : }
     262              : 
     263           56 : QVariant SectionProxyModel::data(const QModelIndex& index, int role) const
     264              : {
     265           56 :     if (!index.isValid() || index.row() < 0 || index.row() >= m_rows.size()) {
     266            0 :         return QVariant();
     267              :     }
     268              : 
     269           56 :     const Row& row = m_rows.at(index.row());
     270              : 
     271           56 :     switch (role) {
     272           18 :     case RoleIsSection: return row.isSection;
     273            9 :     case RoleSectionName: return row.section;
     274            2 :     case RoleSectionExpanded: return !isCollapsed(row.section);
     275            5 :     case RoleSectionItemCount: return row.itemCount;
     276            5 :     case RoleIndexInSection: return row.indexInSection;
     277           17 :     default: break;
     278              :     }
     279              : 
     280           17 :     if (row.isSection) {
     281            0 :         return QVariant();
     282              :     }
     283              : 
     284           17 :     return QAbstractProxyModel::data(index, role);
     285              : }
     286              : 
     287            0 : QHash<int, QByteArray> SectionProxyModel::roleNames() const
     288              : {
     289            0 :     QHash<int, QByteArray> roles = sourceModel() ? sourceModel()->roleNames() : QHash<int, QByteArray>();
     290              : 
     291            0 :     roles.insert(RoleIsSection, "isSection");
     292            0 :     roles.insert(RoleSectionName, "sectionName");
     293            0 :     roles.insert(RoleSectionExpanded, "sectionExpanded");
     294            0 :     roles.insert(RoleSectionItemCount, "sectionItemCount");
     295            0 :     roles.insert(RoleIndexInSection, "indexInSection");
     296              : 
     297            0 :     return roles;
     298            0 : }
     299              : 
     300           22 : void SectionProxyModel::rebuild()
     301              : {
     302           22 :     beginResetModel();
     303              : 
     304           22 :     m_rows.clear();
     305           22 :     m_sourceRowToProxyRow.clear();
     306              : 
     307           22 :     if (sourceModel()) {
     308           22 :         const int roleId = sectionRoleId();
     309           22 :         const int sourceRowCount = sourceModel()->rowCount();
     310              : 
     311           22 :         if (roleId == INVALID_ROLE_ID) {
     312           60 :             for (int i = 0; i < sourceRowCount; ++i) {
     313           50 :                 Row itemRow;
     314           50 :                 itemRow.sourceRow = i;
     315           50 :                 m_rows.append(itemRow);
     316           50 :             }
     317              :         } else {
     318           12 :             QStringList sectionOrder;
     319           12 :             QHash<QString, QList<int> > sourceRowsBySection;
     320              : 
     321           72 :             for (int i = 0; i < sourceRowCount; ++i) {
     322           60 :                 const QString section = sectionOfSourceRow(i, roleId);
     323              : 
     324           60 :                 auto it = sourceRowsBySection.find(section);
     325           60 :                 if (it == sourceRowsBySection.end()) {
     326           36 :                     sectionOrder.append(section);
     327           36 :                     it = sourceRowsBySection.insert(section, QList<int>());
     328              :                 }
     329              : 
     330           60 :                 it.value().append(i);
     331           60 :             }
     332              : 
     333           48 :             for (const QString& section : std::as_const(sectionOrder)) {
     334           36 :                 const QList<int>& sourceRows = sourceRowsBySection.value(section);
     335              : 
     336           36 :                 Row headerRow;
     337           36 :                 headerRow.isSection = true;
     338           36 :                 headerRow.section = section;
     339           36 :                 headerRow.itemCount = sourceRows.size();
     340           36 :                 m_rows.append(headerRow);
     341              : 
     342           36 :                 if (isCollapsed(section)) {
     343            3 :                     continue;
     344              :                 }
     345              : 
     346           88 :                 for (int sourceRow : sourceRows) {
     347           55 :                     Row itemRow;
     348           55 :                     itemRow.section = section;
     349           55 :                     itemRow.sourceRow = sourceRow;
     350           55 :                     m_rows.append(itemRow);
     351           55 :                 }
     352           39 :             }
     353           12 :         }
     354              :     }
     355              : 
     356           22 :     updateRowIndexes();
     357              : 
     358           22 :     endResetModel();
     359              : 
     360           22 :     emit rowCountChanged();
     361           22 : }
     362              : 
     363           26 : void SectionProxyModel::updateRowIndexes()
     364              : {
     365           26 :     m_sourceRowToProxyRow.clear();
     366              : 
     367           26 :     int indexInSection = 0;
     368              : 
     369          195 :     for (int i = 0; i < m_rows.size(); ++i) {
     370          169 :         Row& row = m_rows[i];
     371              : 
     372          169 :         if (row.isSection) {
     373           48 :             indexInSection = 0;
     374           48 :             continue;
     375              :         }
     376              : 
     377          121 :         row.indexInSection = indexInSection++;
     378          121 :         m_sourceRowToProxyRow.insert(row.sourceRow, i);
     379              :     }
     380           26 : }
     381              : 
     382            2 : void SectionProxyModel::onSourceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight,
     383              :                                             const QList<int>& roles)
     384              : {
     385            2 :     const int roleId = sectionRoleId();
     386            2 :     if (roleId != INVALID_ROLE_ID) {
     387            2 :         if (roles.contains(roleId)) {
     388            0 :             rebuild();
     389            0 :             return;
     390              :         }
     391              : 
     392            2 :         if (roles.isEmpty() && isSectionOfRowsChanged(topLeft.row(), bottomRight.row(), roleId)) {
     393            0 :             rebuild();
     394            0 :             return;
     395              :         }
     396              :     }
     397              : 
     398            4 :     for (int sourceRow = topLeft.row(); sourceRow <= bottomRight.row(); ++sourceRow) {
     399            2 :         const QModelIndex proxyIndex = mapFromSource(sourceModel()->index(sourceRow, topLeft.column()));
     400            2 :         if (proxyIndex.isValid()) {
     401            2 :             emit dataChanged(proxyIndex, proxyIndex, roles);
     402              :         }
     403              :     }
     404              : }
     405              : 
     406            2 : bool SectionProxyModel::isSectionOfRowsChanged(int firstSourceRow, int lastSourceRow, int sectionRoleId) const
     407              : {
     408            4 :     for (int sourceRow = firstSourceRow; sourceRow <= lastSourceRow; ++sourceRow) {
     409            2 :         const int proxyRow = m_sourceRowToProxyRow.value(sourceRow, -1);
     410            2 :         if (proxyRow < 0) {
     411            0 :             return true;
     412              :         }
     413              : 
     414            2 :         if (m_rows.at(proxyRow).section != sectionOfSourceRow(sourceRow, sectionRoleId)) {
     415            0 :             return true;
     416              :         }
     417              :     }
     418              : 
     419            2 :     return false;
     420              : }
     421              : 
     422           26 : int SectionProxyModel::sectionRoleId() const
     423              : {
     424           26 :     if (m_sectionRoleName.isEmpty() || !sourceModel()) {
     425           10 :         return INVALID_ROLE_ID;
     426              :     }
     427              : 
     428           16 :     const QByteArray roleName = m_sectionRoleName.toUtf8();
     429           16 :     const QHash<int, QByteArray> roles = sourceModel()->roleNames();
     430              : 
     431           16 :     for (auto it = roles.cbegin(); it != roles.cend(); ++it) {
     432           16 :         if (it.value() == roleName) {
     433           16 :             return it.key();
     434              :         }
     435              :     }
     436              : 
     437            0 :     return INVALID_ROLE_ID;
     438           16 : }
     439              : 
     440           72 : QString SectionProxyModel::sectionOfSourceRow(int sourceRow, int sectionRoleId) const
     441              : {
     442           72 :     if (sectionRoleId == INVALID_ROLE_ID || !sourceModel()) {
     443            0 :         return QString();
     444              :     }
     445              : 
     446           72 :     return sourceModel()->index(sourceRow, 0).data(sectionRoleId).toString();
     447              : }
     448              : 
     449           43 : bool SectionProxyModel::isCollapsed(const QString& section) const
     450              : {
     451           43 :     return m_collapsedSections.contains(section);
     452              : }
     453              : 
     454            4 : int SectionProxyModel::sectionRow(const QString& section) const
     455              : {
     456            4 :     for (int i = 0; i < m_rows.size(); ++i) {
     457            4 :         const Row& row = m_rows.at(i);
     458            4 :         if (row.isSection && row.section == section) {
     459            4 :             return i;
     460              :         }
     461              :     }
     462              : 
     463            0 :     return -1;
     464              : }
        

Generated by: LCOV version 2.0-1