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 : }
|