diff --git a/framework/ui/internal/uiconfiguration.cpp b/framework/ui/internal/uiconfiguration.cpp index d6c86b54da..85d4366700 100644 --- a/framework/ui/internal/uiconfiguration.cpp +++ b/framework/ui/internal/uiconfiguration.cpp @@ -24,9 +24,7 @@ #include #include #include -#include #include -#include #include "uiconfiguration.h" #include "global/configreader.h" @@ -39,10 +37,6 @@ #include "imainwindow.h" -#ifdef Q_OS_WIN -#include -#endif - #include "muse_framework_config.h" #include "log.h" diff --git a/framework/ui/internal/uicontextconfiguration.cpp b/framework/ui/internal/uicontextconfiguration.cpp index 1a93085419..0a36f35f1b 100644 --- a/framework/ui/internal/uicontextconfiguration.cpp +++ b/framework/ui/internal/uicontextconfiguration.cpp @@ -22,8 +22,12 @@ #include "uicontextconfiguration.h" -#include #include +#include + +#ifdef Q_OS_WIN +#include +#endif using namespace muse::ui; diff --git a/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt b/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt index 3ed6bb5d1b..f5aee3b97c 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt +++ b/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt @@ -40,6 +40,8 @@ qt_add_qml_module(muse_uicomponents_qml dropdownview.h filepickermodel.cpp filepickermodel.h + filter.cpp + filter.h filteredflyoutmodel.cpp filteredflyoutmodel.h filtervalue.cpp @@ -74,6 +76,8 @@ qt_add_qml_module(muse_uicomponents_qml selectableitemlistmodel.h selectmultipledirectoriesmodel.cpp selectmultipledirectoriesmodel.h + sorter.cpp + sorter.h sortervalue.cpp sortervalue.h sortfilterproxymodel.cpp diff --git a/framework/uicomponents/qml/Muse/UiComponents/ValueList.qml b/framework/uicomponents/qml/Muse/UiComponents/ValueList.qml index 533b41a2d9..6fe46c2c1e 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/ValueList.qml +++ b/framework/uicomponents/qml/Muse/UiComponents/ValueList.qml @@ -81,21 +81,18 @@ Item { property real spacing: 4 property real sideMargin: 30 - function toggleSorter(sorter) { + function toggleSorter(sorter: Sorter) { if (!sorter.enabled) { - setSorterEnabled(sorter, true) + sorter.sortOrder = Qt.AscendingOrder + sorter.enabled = true } else if (sorter.sortOrder === Qt.AscendingOrder) { sorter.sortOrder = Qt.DescendingOrder } else { - setSorterEnabled(sorter, false) + sorter.enabled = false } selectionModel.clear() } - - function setSorterEnabled(sorter, enable) { - sorter.enabled = enable - } } Rectangle { @@ -187,7 +184,7 @@ Item { } prv.toggleSorter(keySorter) - prv.setSorterEnabled(valueSorter, false) + valueSorter.enabled = false } } @@ -218,7 +215,7 @@ Item { } prv.toggleSorter(valueSorter) - prv.setSorterEnabled(keySorter, false) + keySorter.enabled = false } } } diff --git a/framework/uicomponents/qml/Muse/UiComponents/filter.cpp b/framework/uicomponents/qml/Muse/UiComponents/filter.cpp new file mode 100644 index 0000000000..ee3f406461 --- /dev/null +++ b/framework/uicomponents/qml/Muse/UiComponents/filter.cpp @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "filter.h" + +namespace muse::uicomponents { +Filter::Filter(QObject* parent) + : QObject(parent) +{ +} + +bool Filter::enabled() const +{ + return m_enabled; +} + +void Filter::setEnabled(const bool enabled) +{ + if (m_enabled == enabled) { + return; + } + + m_enabled = enabled; + emit dataChanged(); +} + +bool Filter::async() const +{ + return m_async; +} + +void Filter::setAsync(const bool async) +{ + if (m_async == async) { + return; + } + + m_async = async; + emit dataChanged(); +} +} diff --git a/framework/uicomponents/qml/Muse/UiComponents/filter.h b/framework/uicomponents/qml/Muse/UiComponents/filter.h new file mode 100644 index 0000000000..53b7524340 --- /dev/null +++ b/framework/uicomponents/qml/Muse/UiComponents/filter.h @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +class QModelIndex; + +namespace muse::uicomponents { +class SortFilterProxyModel; + +class Filter : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY dataChanged) + /// Determines whether the SortFilterProxyModel should react asynchronously to the `dataChanged` signal. + Q_PROPERTY(bool async READ async WRITE setAsync NOTIFY dataChanged) + + QML_ELEMENT; + QML_UNCREATABLE("") + +public: + explicit Filter(QObject* parent = nullptr); + + virtual bool acceptsRow(int sourceRow, const QModelIndex& sourceParent, const SortFilterProxyModel&) = 0; + + bool enabled() const; + void setEnabled(bool enabled); + + bool async() const; + void setAsync(bool async); + +signals: + void dataChanged(); + +private: + bool m_enabled = true; + bool m_async = false; +}; +} diff --git a/framework/uicomponents/qml/Muse/UiComponents/filtervalue.cpp b/framework/uicomponents/qml/Muse/UiComponents/filtervalue.cpp index 0c199d8bf8..f6e2abe3d8 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/filtervalue.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/filtervalue.cpp @@ -19,33 +19,44 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + #include "filtervalue.h" +#include "sortfilterproxymodel.h" + using namespace muse::uicomponents; FilterValue::FilterValue(QObject* parent) - : QObject(parent) + : Filter(parent) { } -QString FilterValue::roleName() const +bool FilterValue::acceptsRow(const int sourceRow, const QModelIndex& sourceParent, + const SortFilterProxyModel& proxyModel) { - return m_roleName; -} + const int roleId = proxyModel.roleIdFromName(m_roleName); + if (roleId < 0) { + return true; + } -QVariant FilterValue::roleValue() const -{ - return m_roleValue; -} + const QAbstractItemModel* sourceModel = proxyModel.sourceModel(); + const QModelIndex index = sourceModel->index(sourceRow, 0, sourceParent); + const QVariant data = sourceModel->data(index, roleId); + switch (m_compareType) { + case CompareType::Equal: + return data == m_roleValue; + case CompareType::NotEqual: + return data != m_roleValue; + case CompareType::Contains: + return data.toString().contains(m_roleValue.toString(), Qt::CaseInsensitive); + } -CompareType::Type FilterValue::compareType() const -{ - return m_compareType; + return false; } -bool FilterValue::enabled() const +QString FilterValue::roleName() const { - return m_enabled; + return m_roleName; } void FilterValue::setRoleName(QString roleName) @@ -58,47 +69,32 @@ void FilterValue::setRoleName(QString roleName) emit dataChanged(); } -void FilterValue::setRoleValue(QVariant value) -{ - if (m_roleValue == value) { - return; - } - - m_roleValue = value; - emit dataChanged(); -} - -void FilterValue::setCompareType(CompareType::Type type) +QVariant FilterValue::roleValue() const { - if (m_compareType == type) { - return; - } - - m_compareType = type; - emit dataChanged(); + return m_roleValue; } -void FilterValue::setEnabled(bool enabled) +void FilterValue::setRoleValue(QVariant value) { - if (m_enabled == enabled) { + if (m_roleValue == value) { return; } - m_enabled = enabled; + m_roleValue = value; emit dataChanged(); } -bool FilterValue::async() const +CompareType::Type FilterValue::compareType() const { - return m_async; + return m_compareType; } -void FilterValue::setAsync(bool async) +void FilterValue::setCompareType(CompareType::Type type) { - if (m_async == async) { + if (m_compareType == type) { return; } - m_async = async; + m_compareType = type; emit dataChanged(); } diff --git a/framework/uicomponents/qml/Muse/UiComponents/filtervalue.h b/framework/uicomponents/qml/Muse/UiComponents/filtervalue.h index a4abd2511e..1ace3aa420 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/filtervalue.h +++ b/framework/uicomponents/qml/Muse/UiComponents/filtervalue.h @@ -22,10 +22,11 @@ #pragma once -#include - -#include +#include #include +#include + +#include "filter.h" namespace muse::uicomponents { namespace CompareType { @@ -41,44 +42,32 @@ enum Type Q_ENUM_NS(Type) } -class FilterValue : public QObject +class FilterValue : public Filter { Q_OBJECT - QML_ELEMENT - Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY dataChanged) Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY dataChanged) Q_PROPERTY(muse::uicomponents::CompareType::Type compareType READ compareType WRITE setCompareType NOTIFY dataChanged) - Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY dataChanged) - /// Determines whether the SortFilterProxyModel should react asynchronously to the `dataChanged` signal. - Q_PROPERTY(bool async READ async WRITE setAsync NOTIFY dataChanged) + QML_ELEMENT public: explicit FilterValue(QObject* parent = nullptr); - QString roleName() const; - QVariant roleValue() const; - CompareType::Type compareType() const; - bool enabled() const; + bool acceptsRow(int sourceRow, const QModelIndex& sourceParent, const SortFilterProxyModel&) override; - bool async() const; - void setAsync(bool async); - -public slots: + QString roleName() const; void setRoleName(QString roleName); + + QVariant roleValue() const; void setRoleValue(QVariant roleValue); - void setCompareType(muse::uicomponents::CompareType::Type compareType); - void setEnabled(bool enabled); -signals: - void dataChanged(); + CompareType::Type compareType() const; + void setCompareType(muse::uicomponents::CompareType::Type compareType); private: QString m_roleName; QVariant m_roleValue; CompareType::Type m_compareType = CompareType::Equal; - bool m_enabled = true; - bool m_async = false; }; } diff --git a/framework/uicomponents/qml/Muse/UiComponents/sorter.cpp b/framework/uicomponents/qml/Muse/UiComponents/sorter.cpp new file mode 100644 index 0000000000..decea97acd --- /dev/null +++ b/framework/uicomponents/qml/Muse/UiComponents/sorter.cpp @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "sorter.h" + +namespace muse::uicomponents { +Sorter::Sorter(QObject* parent) + : QObject(parent) +{ +} + +Qt::SortOrder Sorter::sortOrder() const +{ + return m_sortOrder; +} + +void Sorter::setSortOrder(const Qt::SortOrder sortOrder) +{ + if (m_sortOrder == sortOrder) { + return; + } + + m_sortOrder = sortOrder; + emit dataChanged(); +} + +bool Sorter::enabled() const +{ + return m_enabled; +} + +void Sorter::setEnabled(const bool enabled) +{ + if (m_enabled == enabled) { + return; + } + + m_enabled = enabled; + emit dataChanged(); +} +} diff --git a/framework/uicomponents/qml/Muse/UiComponents/sorter.h b/framework/uicomponents/qml/Muse/UiComponents/sorter.h new file mode 100644 index 0000000000..4ac3569a73 --- /dev/null +++ b/framework/uicomponents/qml/Muse/UiComponents/sorter.h @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +class QModelIndex; + +namespace muse::uicomponents { +class SortFilterProxyModel; + +class Sorter : public QObject +{ + Q_OBJECT + Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY dataChanged) + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY dataChanged) + + QML_ELEMENT; + QML_UNCREATABLE("") + +public: + explicit Sorter(QObject* parent = nullptr); + + virtual bool lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const SortFilterProxyModel&) = 0; + + Qt::SortOrder sortOrder() const; + void setSortOrder(Qt::SortOrder sortOrder); + + bool enabled() const; + void setEnabled(bool enabled); + +signals: + void dataChanged(); + +private: + Qt::SortOrder m_sortOrder = Qt::AscendingOrder; + bool m_enabled = false; +}; +} diff --git a/framework/uicomponents/qml/Muse/UiComponents/sortervalue.cpp b/framework/uicomponents/qml/Muse/UiComponents/sortervalue.cpp index 600e952095..54506716e4 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/sortervalue.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/sortervalue.cpp @@ -19,28 +19,40 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + #include "sortervalue.h" +#include "sortfilterproxymodel.h" + using namespace muse::uicomponents; SorterValue::SorterValue(QObject* parent) - : QObject(parent) + : Sorter(parent) { } -QString SorterValue::roleName() const +bool SorterValue::lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, + const SortFilterProxyModel& proxyModel) { - return m_roleName; -} + const int roleId = proxyModel.roleIdFromName(m_roleName); + if (roleId < 0) { + return sourceLeft < sourceRight; + } -Qt::SortOrder SorterValue::sortOrder() const -{ - return m_sortOrder; + const QAbstractItemModel* sourceModel = proxyModel.sourceModel(); + const QVariant leftData = sourceModel->data(sourceLeft, roleId); + const QVariant rightData = sourceModel->data(sourceRight, roleId); + const QPartialOrdering ordering = QVariant::compare(leftData, rightData); + if (ordering == QPartialOrdering::Unordered || ordering == QPartialOrdering::Equivalent) { + return sourceLeft < sourceRight; + } + + return ordering == QPartialOrdering::Less; } -bool SorterValue::enabled() const +QString SorterValue::roleName() const { - return m_enabled; + return m_roleName; } void SorterValue::setRoleName(QString roleName) @@ -52,28 +64,3 @@ void SorterValue::setRoleName(QString roleName) m_roleName = roleName; emit dataChanged(); } - -void SorterValue::setSortOrder(Qt::SortOrder sortOrder) -{ - if (m_sortOrder == sortOrder) { - return; - } - - m_sortOrder = sortOrder; - emit dataChanged(); -} - -void SorterValue::setEnabled(bool enabled) -{ - if (m_enabled == enabled) { - return; - } - - m_enabled = enabled; - - if (!enabled) { - m_sortOrder = Qt::AscendingOrder; - } - - emit dataChanged(); -} diff --git a/framework/uicomponents/qml/Muse/UiComponents/sortervalue.h b/framework/uicomponents/qml/Muse/UiComponents/sortervalue.h index 86d9b46b03..59669984f1 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/sortervalue.h +++ b/framework/uicomponents/qml/Muse/UiComponents/sortervalue.h @@ -22,38 +22,28 @@ #pragma once -#include +#include +#include -#include +#include "sorter.h" namespace muse::uicomponents { -class SorterValue : public QObject +class SorterValue : public Sorter { Q_OBJECT - QML_ELEMENT - Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY dataChanged) - Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY dataChanged) - Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY dataChanged) + + QML_ELEMENT public: explicit SorterValue(QObject* parent = nullptr); - QString roleName() const; - Qt::SortOrder sortOrder() const; - bool enabled() const; + bool lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const SortFilterProxyModel&) override; -public slots: + QString roleName() const; void setRoleName(QString roleName); - void setSortOrder(Qt::SortOrder sortOrder); - void setEnabled(bool enabled); - -signals: - void dataChanged(); private: QString m_roleName; - Qt::SortOrder m_sortOrder = Qt::SortOrder::AscendingOrder; - bool m_enabled = false; }; } diff --git a/framework/uicomponents/qml/Muse/UiComponents/sortfilterproxymodel.cpp b/framework/uicomponents/qml/Muse/UiComponents/sortfilterproxymodel.cpp index cc1e8baa83..8f3a9caf97 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/sortfilterproxymodel.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/sortfilterproxymodel.cpp @@ -19,75 +19,86 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + #include "sortfilterproxymodel.h" +#include + #include +#include -#include "defer.h" -#include "types/val.h" +#include "global/log.h" -#include "view/modelutils.h" +#include "uicomponents/view/modelutils.h" using namespace muse::uicomponents; -static const int INVALID_KEY = -1; +static constexpr int INVALID_ROLE_ID = -1; SortFilterProxyModel::SortFilterProxyModel(QObject* parent) : QSortFilterProxyModel(parent), m_filters(this), m_sorters(this) { ModelUtils::connectRowCountChangedSignal(this, &SortFilterProxyModel::rowCountChanged); + connect(this, &SortFilterProxyModel::sourceModelRoleNamesChanged, this, &SortFilterProxyModel::updateRoleIds); + + const auto invalidateRows = [this] { + beginFilterChange(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) + endFilterChange(Direction::Rows); +#else + invalidateFilter(); +#endif + }; - auto onFilterChanged = [this](FilterValue* changedFilterValue) { - if (changedFilterValue->async()) { - QTimer::singleShot(0, this, [this](){ - fillRoleIds(); - }); + auto onFilterChanged = [this, invalidateRows](Filter* changedFilter) { + if (changedFilter->async()) { + QTimer::singleShot(0, this, invalidateRows); } else { - fillRoleIds(); + invalidateRows(); } }; connect(m_filters.notifier(), &QmlListPropertyNotifier::appended, this, [this, onFilterChanged](int index) { - FilterValue* filter = m_filters.at(index); - + Filter* filter = m_filters.at(index); if (filter->enabled()) { onFilterChanged(filter); } - connect(filter, &FilterValue::dataChanged, this, [onFilterChanged, filter] { onFilterChanged(filter); }); + connect(filter, &Filter::dataChanged, this, [onFilterChanged, filter] { onFilterChanged(filter); }); }); auto onSortersChanged = [this] { - SorterValue* sorter = currentSorterValue(); + Sorter* sorter = currentSorter(); invalidate(); if (!sorter) { + sort(-1, Qt::AscendingOrder); return; } - if (sourceModel()) { - sort(0, sorter->sortOrder()); - } + sort(0, sorter->sortOrder()); }; connect(m_sorters.notifier(), &QmlListPropertyNotifier::appended, this, [this, onSortersChanged](int index) { - onSortersChanged(); + Sorter* sorter = m_sorters.at(index); + if (sorter->enabled()) { + onSortersChanged(); + } - connect(m_sorters.at(index), &SorterValue::dataChanged, this, onSortersChanged); + connect(sorter, &Sorter::dataChanged, this, onSortersChanged); }); connect(this, &SortFilterProxyModel::sourceModelRoleNamesChanged, this, [this]() { invalidate(); - fillRoleIds(); }); } -QQmlListProperty SortFilterProxyModel::filters() +QQmlListProperty SortFilterProxyModel::filters() { return m_filters.property(); } -QQmlListProperty SortFilterProxyModel::sorters() +QQmlListProperty SortFilterProxyModel::sorters() { return m_sorters.property(); } @@ -136,6 +147,11 @@ void SortFilterProxyModel::setAlwaysExcludeIndices(const QList& indices) emit alwaysExcludeIndicesChanged(); } +int SortFilterProxyModel::roleIdFromName(const QString& roleName) const +{ + return m_roleIds.value(roleName.toUtf8(), INVALID_ROLE_ID); +} + QHash SortFilterProxyModel::roleNames() const { if (!sourceModel()) { @@ -161,12 +177,6 @@ void SortFilterProxyModel::setSourceModel(QAbstractItemModel* sourceModel) } } -void SortFilterProxyModel::refresh() -{ - setFilterFixedString(filterRegularExpression().pattern()); - setSortCaseSensitivity(sortCaseSensitivity()); -} - bool SortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { if (m_alwaysIncludeIndices.contains(sourceRow)) { @@ -177,101 +187,26 @@ bool SortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& so return false; } - QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); - - QHashIterator it(m_roleIdToFilterValueHash); - while (it.hasNext()) { - it.next(); - - QVariant data = sourceModel()->data(index, it.key()); - FilterValue* value = it.value(); - - if (!value->enabled()) { - continue; - } - - switch (value->compareType()) { - case CompareType::Equal: - if (data != value->roleValue()) { - return false; - } - break; - case CompareType::NotEqual: - if (data == value->roleValue()) { - return false; - } - break; - case CompareType::Contains: - if (!data.toString().contains(value->roleValue().toString(), Qt::CaseInsensitive)) { - return false; - } - break; - } - } - - return true; + const QList filters = m_filters.list(); + return std::all_of(filters.begin(), filters.end(), [&] (Filter* filter) { + return !filter->enabled() || filter->acceptsRow(sourceRow, sourceParent, *this); + }); } bool SortFilterProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { - SorterValue* sorter = currentSorterValue(); + Sorter* sorter = currentSorter(); if (!sorter) { - return false; + return left < right; } - int sorterRoleKey = roleKey(sorter->roleName()); - - Val leftData = Val::fromQVariant(sourceModel()->data(left, sorterRoleKey)); - Val rightData = Val::fromQVariant(sourceModel()->data(right, sorterRoleKey)); - - return leftData < rightData; -} - -void SortFilterProxyModel::reset() -{ - beginResetModel(); - resetInternalData(); - endResetModel(); -} - -void SortFilterProxyModel::fillRoleIds() -{ - beginFilterChange(); - - DEFER { -#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) - endFilterChange(QSortFilterProxyModel::Direction::Rows); -#else - invalidateFilter(); -#endif - }; - - m_roleIdToFilterValueHash.clear(); - - if (!sourceModel()) { - return; - } - - QHash roleNameToValueHash; - QList filterList = m_filters.list(); - for (FilterValue* filter : std::as_const(filterList)) { - roleNameToValueHash.insert(filter->roleName(), filter); - } - - QHash roles = sourceModel()->roleNames(); - QHash::const_iterator it = roles.constBegin(); - while (it != roles.constEnd()) { - if (roleNameToValueHash.contains(it.value())) { - m_roleIdToFilterValueHash.insert(it.key(), roleNameToValueHash[it.value()]); - } - ++it; - } + return sorter->lessThan(left, right, *this); } -SorterValue* SortFilterProxyModel::currentSorterValue() const +Sorter* SortFilterProxyModel::currentSorter() const { - QList sorterList = m_sorters.list(); - for (SorterValue* sorter : std::as_const(sorterList)) { + const QList sorterList = m_sorters.list(); + for (Sorter* sorter : sorterList) { if (sorter->enabled()) { return sorter; } @@ -280,14 +215,13 @@ SorterValue* SortFilterProxyModel::currentSorterValue() const return nullptr; } -int SortFilterProxyModel::roleKey(const QString& roleName) const +void SortFilterProxyModel::updateRoleIds() { - QHash roles = sourceModel()->roleNames(); - for (auto it = roles.begin(); it != roles.end(); ++it) { - if (roleName == QString(it.value())) { - return it.key(); - } - } + m_roleIds.clear(); - return INVALID_KEY; + const QHash roleNames = this->roleNames(); + for (const auto& [roleId, roleName] : roleNames.asKeyValueRange()) { + const auto [it, didInsert] = m_roleIds.try_emplace(roleName, roleId); + DO_ASSERT_X(didInsert, "duplicate role name"); + } } diff --git a/framework/uicomponents/qml/Muse/UiComponents/sortfilterproxymodel.h b/framework/uicomponents/qml/Muse/UiComponents/sortfilterproxymodel.h index 2b19c8c20f..ccffe5185c 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/sortfilterproxymodel.h +++ b/framework/uicomponents/qml/Muse/UiComponents/sortfilterproxymodel.h @@ -19,34 +19,36 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#pragma once -#include +#pragma once +#include +#include +#include #include +#include -#include "filtervalue.h" -#include "sortervalue.h" +#include "filter.h" #include "qmllistproperty.h" +#include "sorter.h" namespace muse::uicomponents { class SortFilterProxyModel : public QSortFilterProxyModel { Q_OBJECT - QML_ELEMENT - Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged) - - Q_PROPERTY(QQmlListProperty filters READ filters CONSTANT) - Q_PROPERTY(QQmlListProperty sorters READ sorters CONSTANT) + Q_PROPERTY(QQmlListProperty filters READ filters CONSTANT) + Q_PROPERTY(QQmlListProperty sorters READ sorters CONSTANT) Q_PROPERTY(QList alwaysIncludeIndices READ alwaysIncludeIndices WRITE setAlwaysIncludeIndices NOTIFY alwaysIncludeIndicesChanged) Q_PROPERTY(QList alwaysExcludeIndices READ alwaysExcludeIndices WRITE setAlwaysExcludeIndices NOTIFY alwaysExcludeIndicesChanged) + QML_ELEMENT + public: explicit SortFilterProxyModel(QObject* parent = nullptr); - QQmlListProperty filters(); - QQmlListProperty sorters(); + QQmlListProperty filters(); + QQmlListProperty sorters(); QList alwaysIncludeIndices() const; void setAlwaysIncludeIndices(const QList& indices); @@ -54,17 +56,14 @@ class SortFilterProxyModel : public QSortFilterProxyModel QList alwaysExcludeIndices() const; void setAlwaysExcludeIndices(const QList& indices); + int roleIdFromName(const QString&) const; QHash roleNames() const override; void setSourceModel(QAbstractItemModel* sourceModel) override; - Q_INVOKABLE void refresh(); - signals: void rowCountChanged(); - void filtersChanged(QQmlListProperty filters); - void alwaysIncludeIndicesChanged(); void alwaysExcludeIndicesChanged(); @@ -75,16 +74,13 @@ class SortFilterProxyModel : public QSortFilterProxyModel bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; private: - void reset(); - void fillRoleIds(); - - SorterValue* currentSorterValue() const; - int roleKey(const QString& roleName) const; + void updateRoleIds(); + Sorter* currentSorter() const; - QmlListProperty m_filters; - QHash m_roleIdToFilterValueHash; + QHash m_roleIds; - QmlListProperty m_sorters; + QmlListProperty m_filters; + QmlListProperty m_sorters; QList m_alwaysIncludeIndices; QList m_alwaysExcludeIndices; diff --git a/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt b/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt index 7fc0ce714c..32a5c128b7 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt +++ b/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt @@ -22,6 +22,7 @@ set(MODULE_TEST muse_uicomponents_qml_tests) set(MODULE_TEST_SRC ${CMAKE_CURRENT_LIST_DIR}/doubleinputvalidator_tests.cpp + ${CMAKE_CURRENT_LIST_DIR}/sortfilterproxymodel_tests.cpp ) set(MODULE_TEST_LINK diff --git a/framework/uicomponents/qml/Muse/UiComponents/tests/sortfilterproxymodel_tests.cpp b/framework/uicomponents/qml/Muse/UiComponents/tests/sortfilterproxymodel_tests.cpp new file mode 100644 index 0000000000..81bb821552 --- /dev/null +++ b/framework/uicomponents/qml/Muse/UiComponents/tests/sortfilterproxymodel_tests.cpp @@ -0,0 +1,158 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include + +#include +#include +#include + +#include "uicomponents/qml/Muse/UiComponents/filtervalue.h" +#include "uicomponents/qml/Muse/UiComponents/sortervalue.h" +#include "uicomponents/qml/Muse/UiComponents/sortfilterproxymodel.h" + +using namespace Qt::StringLiterals; + +namespace muse::uicomponents { +class UiComponents_SortFilterProxyModelTests : public ::testing::Test +{ +public: + UiComponents_SortFilterProxyModelTests() + { + QStringList modelData; + modelData << u"hay"_s; + modelData << u"needle"_s; + modelData << u"hay needle hay"_s; + modelData << u"needle"_s; + modelData << u"hay hay"_s; + m_model->setStringList(modelData); + } + + QAbstractListModel* listModel() const + { + return m_model.get(); + } + +private: + std::unique_ptr m_model = std::make_unique(); +}; + +TEST_F(UiComponents_SortFilterProxyModelTests, testFilterValue) +{ + QAbstractListModel* model = this->listModel(); + auto proxyModel = std::make_unique(); + proxyModel->setSourceModel(model); + + FilterValue* filter = new FilterValue(proxyModel.get()); + filter->setCompareType(CompareType::Equal); + filter->setRoleName(u"display"_s); + filter->setRoleValue(u"needle"_s); + + QQmlListProperty filters = proxyModel->filters(); + ASSERT_TRUE(filters.append); + filters.append(&filters, filter); + + EXPECT_EQ(proxyModel->rowCount(), 2); + EXPECT_EQ(proxyModel->mapToSource(proxyModel->index(0, 0)), model->index(1)); + EXPECT_EQ(proxyModel->mapToSource(proxyModel->index(1, 0)), model->index(3)); + + filter->setCompareType(CompareType::NotEqual); + EXPECT_EQ(proxyModel->rowCount(), 3); + EXPECT_EQ(proxyModel->mapToSource(proxyModel->index(0, 0)), model->index(0)); + EXPECT_EQ(proxyModel->mapToSource(proxyModel->index(1, 0)), model->index(2)); + EXPECT_EQ(proxyModel->mapToSource(proxyModel->index(2, 0)), model->index(4)); + + filter->setCompareType(CompareType::Contains); + EXPECT_EQ(proxyModel->rowCount(), 3); + EXPECT_EQ(proxyModel->mapToSource(proxyModel->index(0, 0)), model->index(1)); + EXPECT_EQ(proxyModel->mapToSource(proxyModel->index(1, 0)), model->index(2)); + EXPECT_EQ(proxyModel->mapToSource(proxyModel->index(2, 0)), model->index(3)); + + filter->setEnabled(false); + EXPECT_EQ(proxyModel->rowCount(), model->rowCount()); +} + +TEST_F(UiComponents_SortFilterProxyModelTests, testFilterValueMultiple) +{ + QAbstractListModel* model = this->listModel(); + auto proxyModel = std::make_unique(); + proxyModel->setSourceModel(model); + + FilterValue* hayFilter = new FilterValue(proxyModel.get()); + hayFilter->setCompareType(CompareType::Contains); + hayFilter->setRoleName(u"display"_s); + hayFilter->setRoleValue(u"hay"_s); + + FilterValue* needleFilter = new FilterValue(proxyModel.get()); + needleFilter->setCompareType(CompareType::Contains); + needleFilter->setRoleName(u"display"_s); + needleFilter->setRoleValue(u"needle"_s); + + QQmlListProperty filters = proxyModel->filters(); + ASSERT_TRUE(filters.append); + filters.append(&filters, hayFilter); + filters.append(&filters, needleFilter); + + EXPECT_EQ(proxyModel->rowCount(), 1); + EXPECT_EQ(proxyModel->mapToSource(proxyModel->index(0, 0)), model->index(2)); +} + +TEST_F(UiComponents_SortFilterProxyModelTests, testSorterValue) +{ + QAbstractListModel* model = this->listModel(); + auto proxyModel = std::make_unique(); + proxyModel->setSourceModel(model); + + SorterValue* sorter = new SorterValue(proxyModel.get()); + sorter->setRoleName(u"display"_s); + + QQmlListProperty sorters = proxyModel->sorters(); + ASSERT_TRUE(sorters.append); + sorters.append(&sorters, sorter); + + EXPECT_EQ(proxyModel->rowCount(), model->rowCount()); + for (int row = 0; row < proxyModel->rowCount(); ++row) { + SCOPED_TRACE(row); + EXPECT_EQ(proxyModel->mapToSource(proxyModel->index(row, 0)), model->index(row)); + } + + sorter->setEnabled(true); + for (int row = 1; row < proxyModel->rowCount(); ++row) { + SCOPED_TRACE(row); + + const QPartialOrdering ordering = QVariant::compare(proxyModel->data(proxyModel->index(row - 1, 0)), + proxyModel->data(proxyModel->index(row, 0))); + EXPECT_TRUE(ordering == QPartialOrdering::Less || ordering == QPartialOrdering::Equivalent); + } + + sorter->setSortOrder(Qt::DescendingOrder); + for (int row = 1; row < proxyModel->rowCount(); ++row) { + SCOPED_TRACE(row); + + const QPartialOrdering ordering = QVariant::compare(proxyModel->data(proxyModel->index(row - 1, 0)), + proxyModel->data(proxyModel->index(row, 0))); + EXPECT_TRUE(ordering == QPartialOrdering::Greater || ordering == QPartialOrdering::Equivalent); + } +} +}