diff --git a/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt b/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt
index 471a4a7b76..f7a1d00812 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 dcf417ee79..ac4c4523ae 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..3fdb34886f
--- /dev/null
+++ b/framework/uicomponents/qml/Muse/UiComponents/filter.h
@@ -0,0 +1,62 @@
+/*
+ * 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
+ QML_ELEMENT;
+ QML_UNCREATABLE("")
+
+ 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)
+
+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 40e59a4724..ec99dd517a 100644
--- a/framework/uicomponents/qml/Muse/UiComponents/filtervalue.cpp
+++ b/framework/uicomponents/qml/Muse/UiComponents/filtervalue.cpp
@@ -19,33 +19,50 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
+
#include "filtervalue.h"
+#include "global/log.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();
+ IF_ASSERT_FAILED(sourceModel) {
+ return true;
+ }
-CompareType::Type FilterValue::compareType() const
-{
- return m_compareType;
+ 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);
+ }
+
+ return false;
}
-bool FilterValue::enabled() const
+QString FilterValue::roleName() const
{
- return m_enabled;
+ return m_roleName;
}
void FilterValue::setRoleName(QString roleName)
@@ -58,47 +75,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 e66bf68e2a..6e62b3bfb9 100644
--- a/framework/uicomponents/qml/Muse/UiComponents/filtervalue.h
+++ b/framework/uicomponents/qml/Muse/UiComponents/filtervalue.h
@@ -24,9 +24,11 @@
#include
-#include
+#include
#include
+#include "filter.h"
+
namespace muse::uicomponents {
namespace CompareType {
Q_NAMESPACE;
@@ -41,7 +43,7 @@ enum Type
Q_ENUM_NS(Type)
}
-class FilterValue : public QObject
+class FilterValue : public Filter
{
Q_OBJECT
QML_ELEMENT
@@ -49,36 +51,24 @@ class FilterValue : public QObject
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)
public:
explicit FilterValue(QObject* parent = nullptr);
- QString roleName() const;
- QVariant roleValue() const;
- CompareType::Type compareType() const;
- bool enabled() const;
-
- bool async() const;
- void setAsync(bool async);
+ bool acceptsRow(int sourceRow, const QModelIndex& sourceParent, const SortFilterProxyModel&) override;
-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..44429be022
--- /dev/null
+++ b/framework/uicomponents/qml/Muse/UiComponents/sorter.h
@@ -0,0 +1,62 @@
+/*
+ * 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
+ QML_ELEMENT;
+ QML_UNCREATABLE("")
+
+ Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY dataChanged)
+ Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY dataChanged)
+
+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 904436aa59..74816f3a58 100644
--- a/framework/uicomponents/qml/Muse/UiComponents/sortervalue.cpp
+++ b/framework/uicomponents/qml/Muse/UiComponents/sortervalue.cpp
@@ -19,61 +19,54 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
+
#include "sortervalue.h"
+#include "global/log.h"
+
+#include "sortfilterproxymodel.h"
+
using namespace muse::uicomponents;
SorterValue::SorterValue(QObject* parent)
- : QObject(parent)
-{
-}
-
-QString SorterValue::roleName() const
+ : Sorter(parent)
{
- return m_roleName;
}
-Qt::SortOrder SorterValue::sortOrder() const
+bool SorterValue::lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight,
+ const SortFilterProxyModel& proxyModel)
{
- return m_sortOrder;
-}
+ const int roleId = proxyModel.roleIdFromName(m_roleName);
+ if (roleId < 0) {
+ return sourceLeft < sourceRight;
+ }
-bool SorterValue::enabled() const
-{
- return m_enabled;
-}
+ const QAbstractItemModel* sourceModel = proxyModel.sourceModel();
+ IF_ASSERT_FAILED(sourceModel) {
+ return sourceLeft < sourceRight;
+ }
-void SorterValue::setRoleName(QString roleName)
-{
- if (m_roleName == roleName) {
- return;
+ 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;
}
- m_roleName = roleName;
- emit dataChanged();
+ return ordering == QPartialOrdering::Less;
}
-void SorterValue::setSortOrder(Qt::SortOrder sortOrder)
+QString SorterValue::roleName() const
{
- if (m_sortOrder == sortOrder) {
- return;
- }
-
- m_sortOrder = sortOrder;
- emit dataChanged();
+ return m_roleName;
}
-void SorterValue::setEnabled(bool enabled)
+void SorterValue::setRoleName(QString roleName)
{
- if (m_enabled == enabled) {
+ if (m_roleName == roleName) {
return;
}
- m_enabled = enabled;
-
- if (!enabled) {
- m_sortOrder = Qt::AscendingOrder;
- }
-
+ m_roleName = roleName;
emit dataChanged();
}
diff --git a/framework/uicomponents/qml/Muse/UiComponents/sortervalue.h b/framework/uicomponents/qml/Muse/UiComponents/sortervalue.h
index 9854ae08e5..c62697f961 100644
--- a/framework/uicomponents/qml/Muse/UiComponents/sortervalue.h
+++ b/framework/uicomponents/qml/Muse/UiComponents/sortervalue.h
@@ -24,36 +24,27 @@
#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)
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 9eb675b142..473f9ce5d6 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 e5440276bf..92fbe3ac94 100644
--- a/framework/uicomponents/qml/Muse/UiComponents/sortfilterproxymodel.h
+++ b/framework/uicomponents/qml/Muse/UiComponents/sortfilterproxymodel.h
@@ -19,15 +19,19 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
+
#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
@@ -36,17 +40,16 @@ class SortFilterProxyModel : public QSortFilterProxyModel
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)
public:
explicit SortFilterProxyModel(QObject* parent = nullptr);
- QQmlListProperty filters();
- QQmlListProperty sorters();
+ QQmlListProperty filters();
+ QQmlListProperty sorters();
QList alwaysIncludeIndices() const;
void setAlwaysIncludeIndices(const QList& indices);
@@ -54,17 +57,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 +75,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 f5c75cac4a..e3f0f3f8f1 100644
--- a/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt
+++ b/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt
@@ -23,6 +23,7 @@ set(MODULE_TEST muse_uicomponents_qml_tests)
set(MODULE_TEST_SRC
${CMAKE_CURRENT_LIST_DIR}/doubleinputvalidator_tests.cpp
${CMAKE_CURRENT_LIST_DIR}/intinputvalidator_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);
+ }
+}
+}