diff --git a/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.cpp b/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.cpp index 1e8c5b0edd..a518aea529 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.cpp @@ -22,10 +22,11 @@ #include "filteredflyoutmodel.h" -#include "global/translation.h" +#include -using namespace muse::uicomponents; +#include "global/translation.h" +namespace muse::uicomponents { // Recursively traverse a flyout tree, collect all "leaves" (items without a sub item)... static void flattenTreeModel(const QVariant& treeModel, const QString& categoryTitle, QVariantList& result, QVariant& alwaysAppend) { @@ -67,6 +68,25 @@ static void flattenTreeModel(const QVariant& treeModel, const QString& categoryT } } +static bool containsFuzzy(FuzzyMatcher& matcher, const std::u32string_view text, const std::vector& patternTokens) +{ + return std::all_of(patternTokens.begin(), patternTokens.end(), [&](const std::u32string& patternToken) { + const std::size_t tokenSize = patternToken.size(); + if (tokenSize == 0) { + return true; + } + + constexpr std::size_t MIN_TOKEN_SIZE_FOR_FUZZY_MATCH = 4; + constexpr std::size_t CHARS_PER_ERROR = 8; + const std::size_t maxDistance = tokenSize >= MIN_TOKEN_SIZE_FOR_FUZZY_MATCH + ? 1 + (tokenSize / CHARS_PER_ERROR) + : 0; + + matcher.match(text, patternToken, maxDistance); + return !matcher.empty(); + }); +} + FilteredFlyoutModel::FilteredFlyoutModel(QObject* parent) : QObject(parent) { @@ -105,6 +125,15 @@ void FilteredFlyoutModel::setFilterText(const QString& filterText) } m_filterText = filterText; + const QString caseAdjustedPattern = m_filterText.simplified().toLower(); + const QStringList tokens = caseAdjustedPattern.split(u' '); + + m_patternTokens.clear(); + m_patternTokens.reserve(tokens.size()); + for (const auto& token : tokens) { + m_patternTokens.push_back(token.toStdU32String()); + } + QVariantList newModel; newModel.reserve(m_flattenedModel.toList().size()); @@ -113,7 +142,7 @@ void FilteredFlyoutModel::setFilterText(const QString& filterText) for (const QVariant& item : m_flattenedModel.toList()) { QVariantMap itemMap = item.toMap(); const QString title = itemMap.value("title").toString(); - if (!title.contains(m_filterText, Qt::CaseInsensitive)) { + if (!containsFuzzy(m_matcher, title.toLower().toStdU32String(), m_patternTokens)) { continue; } const QString prefix = title.section("-", 0, 0); @@ -140,3 +169,4 @@ void FilteredFlyoutModel::setFilterText(const QString& filterText) emit modelChanged(); } +} diff --git a/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.h b/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.h index c26f0fa12a..c6ac4dfcd2 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.h +++ b/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.h @@ -22,9 +22,17 @@ #pragma once -#include +#include +#include + #include +#include +#include +#include + +#include "global/stringsearch.h" + namespace muse::uicomponents { class FilteredFlyoutModel : public QObject { @@ -49,6 +57,8 @@ class FilteredFlyoutModel : public QObject private: QString m_filterText; + std::vector m_patternTokens; + FuzzyMatcher m_matcher; QVariant m_rawModel;