Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -81,6 +83,12 @@ Column {
compareType: CompareType.Contains
}
]
sorters: [
FuzzyScoreSorter {
enabled: fuzzyFilter.enabled
fuzzyFilter: fuzzyFilter
}
]
}

StyledTextLabel {
Expand Down
1 change: 0 additions & 1 deletion framework/learn/qml/Muse/Learn/LearnPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ FocusScope {
enabled: Boolean(fuzzyPattern)
fuzzyPattern: searchField.searchText
roleName: "searchKey"
caseSensitivity: Qt.CaseInsensitive
}
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ ValueList {
enabled: Boolean(fuzzyPattern)
fuzzyPattern: root.searchText
roleName: "searchKey"
caseSensitivity: Qt.CaseInsensitive
}
]
sorters: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ ValueList {
enabled: Boolean(fuzzyPattern)
fuzzyPattern: root.searchText
roleName: "searchKey"
caseSensitivity: Qt.CaseInsensitive
}
]
sorters: [
Expand Down
110 changes: 52 additions & 58 deletions framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@

#include "fuzzyfilter.h"

#include <algorithm>
#include <iterator>
#include <QChar>

#include "sortfilterproxymodel.h"

Expand All @@ -36,7 +35,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<double> score = getScore(sourceIndex, proxyModel);
const std::optional<double> score = getOrCalcScore(sourceIndex, proxyModel);

return score.has_value();
}
Expand Down Expand Up @@ -83,31 +82,36 @@ void FuzzyFilter::setRoleName(const QString& roleName)
emit dataChanged();
}

Qt::CaseSensitivity FuzzyFilter::caseSensitivity() const
std::optional<double> FuzzyFilter::getScore(const QModelIndex& sourceIndex) const
{
return m_caseSensitivity;
const auto scoreIt = m_scoreCache.find(sourceIndex);
if (scoreIt != m_scoreCache.end()) {
return scoreIt.value();
}

return std::nullopt;
}

void FuzzyFilter::setCaseSensitivity(const Qt::CaseSensitivity caseSensitivity)
void FuzzyFilter::compilePattern()
{
if (m_caseSensitivity == caseSensitivity) {
return;
}
clearScoreCache();

m_caseSensitivity = caseSensitivity;
compilePattern();
emit caseSensitivityChanged();
emit dataChanged();
const QStringList tokens = m_fuzzyPattern.toLower().split(u' ');

m_patternTokens.clear();
m_patternTokens.reserve(tokens.size());
for (const auto& token : tokens) {
m_patternTokens.push_back(token.toStdU32String());
}
}

std::optional<double> FuzzyFilter::getScore(const QPersistentModelIndex& sourceIndex, const SortFilterProxyModel& proxyModel)
std::optional<double> FuzzyFilter::getOrCalcScore(const QModelIndex& sourceIndex, const SortFilterProxyModel& proxyModel)
{
auto scoreIt = m_scoreCache.find(sourceIndex);
if (scoreIt != m_scoreCache.end()) {
return scoreIt.value();
if (const std::optional<double> score = getScore(sourceIndex)) {
return score;
}

std::optional<double> score = calcScore(sourceIndex, proxyModel);
const std::optional<double> 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
Expand All @@ -120,22 +124,6 @@ std::optional<double> FuzzyFilter::getScore(const QPersistentModelIndex& sourceI
return score;
}

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();
});
}

std::optional<double> FuzzyFilter::calcScore(const QModelIndex& sourceIndex, const SortFilterProxyModel& proxyModel)
{
const int role = proxyModel.roleIdFromName(m_roleName);
Expand All @@ -145,9 +133,7 @@ std::optional<double> 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) {
Expand All @@ -162,36 +148,44 @@ std::optional<double> FuzzyFilter::calcScore(const QModelIndex& sourceIndex, con
? 1 + (tokenSize / CHARS_PER_ERROR)
: 0;

const double inverseTokenSize = 1.0 / tokenSize;
std::optional<double> bestTokenScore;

const double perCharScore = 1.0 / tokenSize;
std::optional<double> tokenScore;
for (const auto& match : m_matcher(text, patternToken, maxDistance)) {
const double matchSimilarity = 1.0 - (match.editDistance * inverseTokenSize);
double matchScore = 5.0 * matchSimilarity;

const bool isMatchStartAtStartOfWord = match.beginPos == 0
|| text[match.beginPos - 1] == U' ';
if (isMatchStartAtStartOfWord) {
const bool isMatchEndAtEndOfWord = match.endPos == text.size()
|| text[match.endPos] == U' ';
if (isMatchEndAtEndOfWord) {
matchScore += 2.0 * inverseTokenSize;
} else {
matchScore += inverseTokenSize;
const double matchSimilarity = 1.0 - (match.editDistance * perCharScore);

const double scoreBonus = [&] {
const bool isFullMatch = (match.endPos - match.beginPos) == text.size();
if (isFullMatch) {
return 3.0 * perCharScore;
}
}

if (bestTokenScore < matchScore) {
bestTokenScore = matchScore;
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;
}
}

// 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;
Expand Down
9 changes: 2 additions & 7 deletions framework/uicomponents/qml/Muse/UiComponents/fuzzyfilter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -57,25 +56,21 @@ class FuzzyFilter : public Filter
QString roleName() const;
void setRoleName(const QString&);

Qt::CaseSensitivity caseSensitivity() const;
void setCaseSensitivity(Qt::CaseSensitivity);

std::optional<double> getScore(const QPersistentModelIndex& sourceIndex, const SortFilterProxyModel&);
std::optional<double> getScore(const QModelIndex& sourceIndex) const;

signals:
void fuzzyPatternChanged();
void roleNameChanged();
void caseSensitivityChanged();

private:
void compilePattern();

std::optional<double> getOrCalcScore(const QModelIndex& sourceIndex, const SortFilterProxyModel&);
std::optional<double> calcScore(const QModelIndex& sourceIndex, const SortFilterProxyModel&);
void clearScoreCache();

QString m_fuzzyPattern;
QString m_roleName;
Qt::CaseSensitivity m_caseSensitivity = Qt::CaseSensitive;

std::vector<std::u32string> m_patternTokens;
FuzzyMatcher m_matcher;
Expand Down
58 changes: 54 additions & 4 deletions framework/uicomponents/qml/Muse/UiComponents/fuzzyscoresorter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,33 @@

#include "fuzzyscoresorter.h"

#include "sortfilterproxymodel.h"

#include <algorithm>

namespace muse::uicomponents {
namespace {
std::optional<double> getMaxScoreFromChildren(const FuzzyFilter& fuzzyFilter, const QAbstractItemModel& model,
const QModelIndex& parentIndex)
{
const int childRowCount = model.rowCount(parentIndex);

std::optional<double> maxChildScore;
for (int i = 0; i < childRowCount; ++i) {
const QModelIndex childIndex = model.index(i, 0, parentIndex);
const std::optional<double> maxGrandChildrenScore = getMaxScoreFromChildren(fuzzyFilter, model, childIndex);
const std::optional<double> childScore = std::max(maxGrandChildrenScore, fuzzyFilter.getScore(childIndex));

maxChildScore = std::max(maxChildScore, childScore);
}

return maxChildScore;
}
}

FuzzyScoreSorter::FuzzyScoreSorter(QObject* parent)
: Sorter(parent)
{
setSortOrder(Qt::DescendingOrder);
}

bool FuzzyScoreSorter::lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight,
Expand All @@ -36,10 +58,38 @@ bool FuzzyScoreSorter::lessThan(const QModelIndex& sourceLeft, const QModelIndex
return sourceLeft < sourceRight;
}

const std::optional<double> leftScore = m_fuzzyFilter->getScore(sourceLeft, proxyModel);
const std::optional<double> rightScore = m_fuzzyFilter->getScore(sourceRight, proxyModel);
// 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<double> leftScore = m_fuzzyFilter->getScore(sourceLeft);
const std::optional<double> rightScore = m_fuzzyFilter->getScore(sourceRight);

if (!proxyModel.isRecursiveFilteringEnabled()) {
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 their own score or the highest child score, whichever is higher

const QAbstractItemModel& srcModel = *proxyModel.sourceModel();

const std::optional<double> maxLeftChildScore = getMaxScoreFromChildren(*m_fuzzyFilter, srcModel, sourceLeft);
const std::optional<double> maxRightChildScore = getMaxScoreFromChildren(*m_fuzzyFilter, srcModel, sourceRight);
Comment thread
juli27 marked this conversation as resolved.

return leftScore < rightScore;
return std::max(leftScore, maxLeftChildScore) > std::max(rightScore, maxRightChildScore);
}

FuzzyFilter* FuzzyScoreSorter::fuzzyFilter() const
Expand Down