From 7d6343cad6a5bacb80dc486e37c704acd0355a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20B=C3=BChler?= Date: Sun, 30 Aug 2026 15:15:54 +0200 Subject: [PATCH 1/7] uicomponents: Cleanup FuzzyFilter --- .../qml/Muse/UiComponents/fuzzyfilter.cpp | 73 ++++++++++--------- .../qml/Muse/UiComponents/fuzzyfilter.h | 3 +- .../Muse/UiComponents/fuzzyscoresorter.cpp | 6 +- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp b/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp index 4ba466a3f0..c8cdb622b8 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp @@ -22,9 +22,6 @@ #include "fuzzyfilter.h" -#include -#include - #include "sortfilterproxymodel.h" namespace muse::uicomponents { @@ -36,7 +33,7 @@ FuzzyFilter::FuzzyFilter(QObject* parent) bool FuzzyFilter::acceptsRow(int sourceRow, const QModelIndex& sourceParent, const SortFilterProxyModel& proxyModel) { const QModelIndex sourceIndex = proxyModel.sourceModel()->index(sourceRow, 0, sourceParent); - const std::optional score = getScore(sourceIndex, proxyModel); + const std::optional score = getOrCalcScore(sourceIndex, proxyModel); return score.has_value(); } @@ -100,40 +97,49 @@ void FuzzyFilter::setCaseSensitivity(const Qt::CaseSensitivity caseSensitivity) emit dataChanged(); } -std::optional FuzzyFilter::getScore(const QPersistentModelIndex& sourceIndex, const SortFilterProxyModel& proxyModel) +std::optional FuzzyFilter::getScore(const QModelIndex& sourceIndex) const { - auto scoreIt = m_scoreCache.find(sourceIndex); + const auto scoreIt = m_scoreCache.find(sourceIndex); if (scoreIt != m_scoreCache.end()) { return scoreIt.value(); } - std::optional score = calcScore(sourceIndex, proxyModel); - // don't cache score of filtered out items because the cache - // is always reset before filtering and therefore only used for sorting - // already filtered items - if (!score) { - return score; - } - - m_scoreCache.try_emplace(sourceIndex, *score); - - return score; + return std::nullopt; } void FuzzyFilter::compilePattern() { clearScoreCache(); - m_patternTokens.clear(); - const QString caseAdjustedPattern = caseSensitivity() == Qt::CaseInsensitive ? m_fuzzyPattern.toLower() : m_fuzzyPattern; - const QStringList tokens = caseAdjustedPattern.split(u' '); - std::transform(tokens.begin(), tokens.end(), std::back_inserter(m_patternTokens), [](const QString& token) { - return token.toStdU32String(); - }); + + m_patternTokens.clear(); + m_patternTokens.reserve(tokens.size()); + for (const auto& token : tokens) { + m_patternTokens.push_back(token.toStdU32String()); + } +} + +std::optional FuzzyFilter::getOrCalcScore(const QModelIndex& sourceIndex, const SortFilterProxyModel& proxyModel) +{ + if (const std::optional score = getScore(sourceIndex)) { + return score; + } + + const std::optional score = calcScore(sourceIndex, proxyModel); + // don't cache score of filtered out items because the cache + // is always reset before filtering and therefore only used for sorting + // already filtered items + if (!score) { + return score; + } + + m_scoreCache.try_emplace(sourceIndex, *score); + + return score; } std::optional FuzzyFilter::calcScore(const QModelIndex& sourceIndex, const SortFilterProxyModel& proxyModel) @@ -162,11 +168,10 @@ std::optional FuzzyFilter::calcScore(const QModelIndex& sourceIndex, con ? 1 + (tokenSize / CHARS_PER_ERROR) : 0; - const double inverseTokenSize = 1.0 / tokenSize; - std::optional bestTokenScore; - + const double perCharScore = 1.0 / tokenSize; + std::optional tokenScore; for (const auto& match : m_matcher(text, patternToken, maxDistance)) { - const double matchSimilarity = 1.0 - (match.editDistance * inverseTokenSize); + const double matchSimilarity = 1.0 - (match.editDistance * perCharScore); double matchScore = 5.0 * matchSimilarity; const bool isMatchStartAtStartOfWord = match.beginPos == 0 @@ -175,23 +180,23 @@ std::optional FuzzyFilter::calcScore(const QModelIndex& sourceIndex, con const bool isMatchEndAtEndOfWord = match.endPos == text.size() || text[match.endPos] == U' '; if (isMatchEndAtEndOfWord) { - matchScore += 2.0 * inverseTokenSize; + matchScore += 2.0 * perCharScore; } else { - matchScore += inverseTokenSize; + matchScore += perCharScore; } } - if (bestTokenScore < matchScore) { - bestTokenScore = matchScore; + if (tokenScore < matchScore) { + tokenScore = matchScore; } } // no match for token found -> no score for entire pattern - if (!bestTokenScore) { - return bestTokenScore; + if (!tokenScore) { + return std::nullopt; } - score += *bestTokenScore; + score += *tokenScore; } return score; diff --git a/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.h b/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.h index e498df71a4..e55ae5f489 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.h +++ b/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.h @@ -60,7 +60,7 @@ class FuzzyFilter : public Filter Qt::CaseSensitivity caseSensitivity() const; void setCaseSensitivity(Qt::CaseSensitivity); - std::optional getScore(const QPersistentModelIndex& sourceIndex, const SortFilterProxyModel&); + std::optional getScore(const QModelIndex& sourceIndex) const; signals: void fuzzyPatternChanged(); @@ -70,6 +70,7 @@ class FuzzyFilter : public Filter private: void compilePattern(); + std::optional getOrCalcScore(const QModelIndex& sourceIndex, const SortFilterProxyModel&); std::optional calcScore(const QModelIndex& sourceIndex, const SortFilterProxyModel&); void clearScoreCache(); diff --git a/framework/uicomponents/qml/Muse/UiComponents/fuzzyscoresorter.cpp b/framework/uicomponents/qml/Muse/UiComponents/fuzzyscoresorter.cpp index 055ca7805b..a2756d4210 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/fuzzyscoresorter.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/fuzzyscoresorter.cpp @@ -30,14 +30,14 @@ FuzzyScoreSorter::FuzzyScoreSorter(QObject* parent) } bool FuzzyScoreSorter::lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, - const SortFilterProxyModel& proxyModel) + const SortFilterProxyModel& /*proxyModel*/) { if (!m_fuzzyFilter) { return sourceLeft < sourceRight; } - const std::optional leftScore = m_fuzzyFilter->getScore(sourceLeft, proxyModel); - const std::optional rightScore = m_fuzzyFilter->getScore(sourceRight, proxyModel); + const std::optional leftScore = m_fuzzyFilter->getScore(sourceLeft); + const std::optional rightScore = m_fuzzyFilter->getScore(sourceRight); return leftScore < rightScore; } From b92747380c24270b816af4e87b370c528cb1ce08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20B=C3=BChler?= Date: Sun, 30 Aug 2026 16:31:46 +0200 Subject: [PATCH 2/7] uicomponents: Remove caseSensitivity property from FuzzyFilter Fuzzy searching with case sensitive matching makes little sense. So let's remove the property entirely now, before the class is used outside of the framework. (Alternative: change default to Qt::CaseInsensitive) --- framework/learn/qml/Muse/Learn/LearnPage.qml | 1 - .../Muse/Shortcuts/internal/ShortcutsList.qml | 1 - .../Muse/Shortcuts/internal/ShortcutsList.qml | 1 - .../qml/Muse/UiComponents/fuzzyfilter.cpp | 26 ++----------------- .../qml/Muse/UiComponents/fuzzyfilter.h | 6 ----- 5 files changed, 2 insertions(+), 33 deletions(-) diff --git a/framework/learn/qml/Muse/Learn/LearnPage.qml b/framework/learn/qml/Muse/Learn/LearnPage.qml index 61e8c527d4..7021c2f72b 100644 --- a/framework/learn/qml/Muse/Learn/LearnPage.qml +++ b/framework/learn/qml/Muse/Learn/LearnPage.qml @@ -209,7 +209,6 @@ FocusScope { enabled: Boolean(fuzzyPattern) fuzzyPattern: searchField.searchText roleName: "searchKey" - caseSensitivity: Qt.CaseInsensitive } ] diff --git a/framework/shortcuts/qml/Muse/Shortcuts/internal/ShortcutsList.qml b/framework/shortcuts/qml/Muse/Shortcuts/internal/ShortcutsList.qml index fb87612cb2..34eabcf37e 100644 --- a/framework/shortcuts/qml/Muse/Shortcuts/internal/ShortcutsList.qml +++ b/framework/shortcuts/qml/Muse/Shortcuts/internal/ShortcutsList.qml @@ -58,7 +58,6 @@ ValueList { enabled: Boolean(fuzzyPattern) fuzzyPattern: root.searchText roleName: "searchKey" - caseSensitivity: Qt.CaseInsensitive } ] sorters: [ diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsList.qml b/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsList.qml index c8d479d57a..25edf96860 100644 --- a/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsList.qml +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsList.qml @@ -78,7 +78,6 @@ ValueList { enabled: Boolean(fuzzyPattern) fuzzyPattern: root.searchText roleName: "searchKey" - caseSensitivity: Qt.CaseInsensitive } ] sorters: [ diff --git a/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp b/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp index c8cdb622b8..4101254301 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp @@ -80,23 +80,6 @@ void FuzzyFilter::setRoleName(const QString& roleName) emit dataChanged(); } -Qt::CaseSensitivity FuzzyFilter::caseSensitivity() const -{ - return m_caseSensitivity; -} - -void FuzzyFilter::setCaseSensitivity(const Qt::CaseSensitivity caseSensitivity) -{ - if (m_caseSensitivity == caseSensitivity) { - return; - } - - m_caseSensitivity = caseSensitivity; - compilePattern(); - emit caseSensitivityChanged(); - emit dataChanged(); -} - std::optional FuzzyFilter::getScore(const QModelIndex& sourceIndex) const { const auto scoreIt = m_scoreCache.find(sourceIndex); @@ -111,10 +94,7 @@ void FuzzyFilter::compilePattern() { clearScoreCache(); - const QString caseAdjustedPattern = caseSensitivity() == Qt::CaseInsensitive - ? m_fuzzyPattern.toLower() - : m_fuzzyPattern; - const QStringList tokens = caseAdjustedPattern.split(u' '); + const QStringList tokens = m_fuzzyPattern.toLower().split(u' '); m_patternTokens.clear(); m_patternTokens.reserve(tokens.size()); @@ -151,9 +131,7 @@ std::optional FuzzyFilter::calcScore(const QModelIndex& sourceIndex, con const QString rawText = proxyModel.sourceModel()->data(sourceIndex, role) .toString(); - const std::u32string text = caseSensitivity() == Qt::CaseInsensitive - ? rawText.toLower().toStdU32String() - : rawText.toStdU32String(); + const std::u32string text = rawText.toLower().toStdU32String(); double score = 0.0; for (const auto& patternToken : m_patternTokens) { diff --git a/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.h b/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.h index e55ae5f489..c726c180ef 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.h +++ b/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.h @@ -43,7 +43,6 @@ class FuzzyFilter : public Filter Q_PROPERTY(QString fuzzyPattern READ fuzzyPattern WRITE setFuzzyPattern NOTIFY fuzzyPatternChanged) Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged) - Q_PROPERTY(Qt::CaseSensitivity caseSensitivity READ caseSensitivity WRITE setCaseSensitivity NOTIFY caseSensitivityChanged) public: explicit FuzzyFilter(QObject* parent = nullptr); @@ -57,15 +56,11 @@ class FuzzyFilter : public Filter QString roleName() const; void setRoleName(const QString&); - Qt::CaseSensitivity caseSensitivity() const; - void setCaseSensitivity(Qt::CaseSensitivity); - std::optional getScore(const QModelIndex& sourceIndex) const; signals: void fuzzyPatternChanged(); void roleNameChanged(); - void caseSensitivityChanged(); private: void compilePattern(); @@ -76,7 +71,6 @@ class FuzzyFilter : public Filter QString m_fuzzyPattern; QString m_roleName; - Qt::CaseSensitivity m_caseSensitivity = Qt::CaseSensitive; std::vector m_patternTokens; FuzzyMatcher m_matcher; From 04953321e168e4e3d10194b7324311ea08bf1e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20B=C3=BChler?= Date: Sat, 29 Aug 2026 15:58:01 +0200 Subject: [PATCH 3/7] extensions: Use fuzzy search --- .../Extensions/internal/ExtensionsListView.qml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/framework/extensions/qml/Muse/Extensions/internal/ExtensionsListView.qml b/framework/extensions/qml/Muse/Extensions/internal/ExtensionsListView.qml index af7f7b9f6f..b83b255141 100644 --- a/framework/extensions/qml/Muse/Extensions/internal/ExtensionsListView.qml +++ b/framework/extensions/qml/Muse/Extensions/internal/ExtensionsListView.qml @@ -65,10 +65,12 @@ Column { id: filterModel filters: [ - FilterValue { + FuzzyFilter { + id: fuzzyFilter + + enabled: Boolean(fuzzyPattern) + fuzzyPattern: root.search roleName: "name" - roleValue: root.search - compareType: CompareType.Contains }, FilterValue { roleName: "enabled" @@ -81,6 +83,12 @@ Column { compareType: CompareType.Contains } ] + sorters: [ + FuzzyScoreSorter { + enabled: fuzzyFilter.enabled + fuzzyFilter: fuzzyFilter + } + ] } StyledTextLabel { From c2a47c33a4e353c745a6055650cbff0474c13eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20B=C3=BChler?= Date: Sun, 30 Aug 2026 15:58:04 +0200 Subject: [PATCH 4/7] uicomponents: Improve FuzzyFilter match scoring Recognize all non-letter characters as a word boundary. This improves the palette search results for dynamics, e.g.: searching for "piano" will correctly count "p (piano)" as a full word match. --- .../uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp b/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp index 4101254301..f63b0283d9 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp @@ -22,6 +22,8 @@ #include "fuzzyfilter.h" +#include + #include "sortfilterproxymodel.h" namespace muse::uicomponents { @@ -153,10 +155,10 @@ std::optional FuzzyFilter::calcScore(const QModelIndex& sourceIndex, con double matchScore = 5.0 * matchSimilarity; const bool isMatchStartAtStartOfWord = match.beginPos == 0 - || text[match.beginPos - 1] == U' '; + || !QChar::isLetter(text[match.beginPos - 1]); if (isMatchStartAtStartOfWord) { const bool isMatchEndAtEndOfWord = match.endPos == text.size() - || text[match.endPos] == U' '; + || !QChar::isLetter(text[match.endPos]); if (isMatchEndAtEndOfWord) { matchScore += 2.0 * perCharScore; } else { From 1f203cbbbe29fc9b58d0e3853084bfbbdf7d4324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20B=C3=BChler?= Date: Sun, 30 Aug 2026 16:13:17 +0200 Subject: [PATCH 5/7] uicomponents: Add support for tree models to FuzzyScoreSorter Take the score of child items into account when sorting a model with recursive filtering enabled. This places parent items with better child matches before parents with worse matches. --- .../Muse/UiComponents/fuzzyscoresorter.cpp | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/framework/uicomponents/qml/Muse/UiComponents/fuzzyscoresorter.cpp b/framework/uicomponents/qml/Muse/UiComponents/fuzzyscoresorter.cpp index a2756d4210..7cecbb1f5c 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/fuzzyscoresorter.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/fuzzyscoresorter.cpp @@ -22,7 +22,30 @@ #include "fuzzyscoresorter.h" +#include "sortfilterproxymodel.h" + +#include + namespace muse::uicomponents { +namespace { +std::optional getMaxScoreFromChildren(const FuzzyFilter& fuzzyFilter, const QAbstractItemModel& model, + const QModelIndex& parentIndex) +{ + const int childRowCount = model.rowCount(parentIndex); + + std::optional maxChildScore; + for (int i = 0; i < childRowCount; ++i) { + const QModelIndex childIndex = model.index(i, 0, parentIndex); + const std::optional maxGrandChildrenScore = getMaxScoreFromChildren(fuzzyFilter, model, childIndex); + const std::optional childScore = std::max(maxGrandChildrenScore, fuzzyFilter.getScore(childIndex)); + + maxChildScore = std::max(maxChildScore, childScore); + } + + return maxChildScore; +} +} + FuzzyScoreSorter::FuzzyScoreSorter(QObject* parent) : Sorter(parent) { @@ -30,7 +53,7 @@ FuzzyScoreSorter::FuzzyScoreSorter(QObject* parent) } bool FuzzyScoreSorter::lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, - const SortFilterProxyModel& /*proxyModel*/) + const SortFilterProxyModel& proxyModel) { if (!m_fuzzyFilter) { return sourceLeft < sourceRight; @@ -39,7 +62,18 @@ bool FuzzyScoreSorter::lessThan(const QModelIndex& sourceLeft, const QModelIndex const std::optional leftScore = m_fuzzyFilter->getScore(sourceLeft); const std::optional rightScore = m_fuzzyFilter->getScore(sourceRight); - return leftScore < rightScore; + if (!proxyModel.isRecursiveFilteringEnabled()) { + return leftScore < rightScore; + } + + // rank parent items according to the highest child score + + const QAbstractItemModel& srcModel = *proxyModel.sourceModel(); + + const std::optional maxLeftChildScore = getMaxScoreFromChildren(*m_fuzzyFilter, srcModel, sourceLeft); + const std::optional maxRightChildScore = getMaxScoreFromChildren(*m_fuzzyFilter, srcModel, sourceRight); + + return std::max(leftScore, maxLeftChildScore) < std::max(rightScore, maxRightChildScore); } FuzzyFilter* FuzzyScoreSorter::fuzzyFilter() const From 35a0d9604f9532b47df1d1e6c9afe8eb27524006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20B=C3=BChler?= Date: Sun, 6 Sep 2026 22:27:39 +0200 Subject: [PATCH 6/7] uicomponents: Fix full fuzzy matches not being favored Add a bonus score when the pattern matches on the entire text. This bonus is higher the the word bonus. --- .../qml/Muse/UiComponents/fuzzyfilter.cpp | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp b/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp index f63b0283d9..05ef45ac75 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp @@ -152,20 +152,29 @@ std::optional FuzzyFilter::calcScore(const QModelIndex& sourceIndex, con std::optional tokenScore; for (const auto& match : m_matcher(text, patternToken, maxDistance)) { const double matchSimilarity = 1.0 - (match.editDistance * perCharScore); - double matchScore = 5.0 * matchSimilarity; - - const bool isMatchStartAtStartOfWord = match.beginPos == 0 - || !QChar::isLetter(text[match.beginPos - 1]); - if (isMatchStartAtStartOfWord) { - const bool isMatchEndAtEndOfWord = match.endPos == text.size() - || !QChar::isLetter(text[match.endPos]); - if (isMatchEndAtEndOfWord) { - matchScore += 2.0 * perCharScore; - } else { - matchScore += perCharScore; + + const double scoreBonus = [&] { + const bool isFullMatch = (match.endPos - match.beginPos) == text.size(); + if (isFullMatch) { + return 3.0 * perCharScore; } - } + const bool isMatchStartAtStartOfWord = match.beginPos == 0 + || !QChar::isLetter(text[match.beginPos - 1]); + if (isMatchStartAtStartOfWord) { + const bool isMatchEndAtEndOfWord = match.endPos == text.size() + || !QChar::isLetter(text[match.endPos]); + if (isMatchEndAtEndOfWord) { + return 2.0 * perCharScore; + } else { + return perCharScore; + } + } + + return 0.0; + }(); + + const double matchScore = 5.0 * matchSimilarity + scoreBonus; if (tokenScore < matchScore) { tokenScore = matchScore; } From afd732b774539618728fa168fb5d4c26a1314258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20B=C3=BChler?= Date: Sun, 6 Sep 2026 22:39:02 +0200 Subject: [PATCH 7/7] uicomponents: Improve tree model fuzzy score sort behavior - Don't sort children when their parent has a match - prefer parents that match over parents with only children that match --- .../Muse/UiComponents/fuzzyscoresorter.cpp | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/framework/uicomponents/qml/Muse/UiComponents/fuzzyscoresorter.cpp b/framework/uicomponents/qml/Muse/UiComponents/fuzzyscoresorter.cpp index 7cecbb1f5c..b468231b79 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/fuzzyscoresorter.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/fuzzyscoresorter.cpp @@ -49,7 +49,6 @@ std::optional getMaxScoreFromChildren(const FuzzyFilter& fuzzyFilter, co FuzzyScoreSorter::FuzzyScoreSorter(QObject* parent) : Sorter(parent) { - setSortOrder(Qt::DescendingOrder); } bool FuzzyScoreSorter::lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, @@ -59,21 +58,38 @@ bool FuzzyScoreSorter::lessThan(const QModelIndex& sourceLeft, const QModelIndex return sourceLeft < sourceRight; } + // don't sort children when their parent has a score + // checking for one parent is enough. left and right always have the same parent + if (const QModelIndex leftParent = sourceLeft.parent(); leftParent.isValid()) { + if (m_fuzzyFilter->getScore(leftParent)) { + return sourceLeft < sourceRight; + } + } + const std::optional leftScore = m_fuzzyFilter->getScore(sourceLeft); const std::optional rightScore = m_fuzzyFilter->getScore(sourceRight); if (!proxyModel.isRecursiveFilteringEnabled()) { - return leftScore < rightScore; + return leftScore > rightScore; + } + + // prefer parents that match over parents with only children that match + if (leftScore && !rightScore) { + return true; + } + + if (!leftScore && rightScore) { + return false; } - // rank parent items according to the highest child score + // rank parent items according to their own score or the highest child score, whichever is higher const QAbstractItemModel& srcModel = *proxyModel.sourceModel(); const std::optional maxLeftChildScore = getMaxScoreFromChildren(*m_fuzzyFilter, srcModel, sourceLeft); const std::optional maxRightChildScore = getMaxScoreFromChildren(*m_fuzzyFilter, srcModel, sourceRight); - return std::max(leftScore, maxLeftChildScore) < std::max(rightScore, maxRightChildScore); + return std::max(leftScore, maxLeftChildScore) > std::max(rightScore, maxRightChildScore); } FuzzyFilter* FuzzyScoreSorter::fuzzyFilter() const