Skip to content

Commit

Permalink
Add fuzzy search to all possible locator filters; Move to StringUtils…
Browse files Browse the repository at this point in the history
…::fuzzySearch; Add some tests (that helped!)
  • Loading branch information
suricactus authored and nyalldawson committed Mar 17, 2020
1 parent 5241d64 commit 9e75e3b
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 66 deletions.
11 changes: 11 additions & 0 deletions python/core/auto_generated/qgsstringutils.sip.in
Expand Up @@ -248,6 +248,17 @@ so strings with similar sounds should be represented by the same Soundex code.
:param string: input string

:return: 4 letter Soundex code
%End

static double fuzzyScore( const QString &candidate, const QString &search );
%Docstring
Tests a ``candidate`` string to see how likely it is a match for
a specified ``search`` string. Values are normalized between 0 and 1.

:param candidate: candidate string
:param search: search term string

.. versionadded:: 3.14
%End

static QString insertLinks( const QString &string, bool *foundLinks = 0 );
Expand Down
34 changes: 22 additions & 12 deletions python/plugins/processing/gui/AlgorithmLocatorFilter.py
Expand Up @@ -30,7 +30,8 @@
QgsProcessing,
QgsWkbTypes,
QgsMapLayerType,
QgsFields)
QgsFields,
QgsStringUtils)
from processing.gui.MessageBarProgress import MessageBarProgress
from processing.gui.MessageDialog import MessageDialog
from processing.gui.AlgorithmDialog import AlgorithmDialog
Expand Down Expand Up @@ -72,17 +73,26 @@ def fetchResults(self, string, context, feedback):
a.flags() & QgsProcessingAlgorithm.FlagKnownIssues:
continue

if QgsLocatorFilter.stringMatches(a.displayName(), string) or [t for t in a.tags() if QgsLocatorFilter.stringMatches(t, string)] or \
(context.usingPrefix and not string):
result = QgsLocatorResult()
result.filter = self
result.displayString = a.displayName()
result.icon = a.icon()
result.userData = a.id()
if string and QgsLocatorFilter.stringMatches(a.displayName(), string):
result.score = float(len(string)) / len(a.displayName())
else:
result.score = 0
result = QgsLocatorResult()
result.filter = self
result.displayString = a.displayName()
result.icon = a.icon()
result.userData = a.id()
result.score = 0

if (context.usingPrefix and not string):
self.resultFetched.emit(result)

for t in a.tags():
result.score = QgsStringUtils.fuzzyMatch(t, string)

if result.score > 0:
self.resultFetched.emit(result)
continue

result.score = QgsStringUtils.fuzzyMatch(result.displayString, string)

if result.score > 0:
self.resultFetched.emit(result)

def triggerResult(self, result):
Expand Down
99 changes: 55 additions & 44 deletions src/app/locator/qgsinbuiltlocatorfilters.cpp
Expand Up @@ -57,23 +57,17 @@ void QgsLayerTreeLocatorFilter::fetchResults( const QString &string, const QgsLo
result.userData = layer->layerId();
result.icon = QgsMapLayerModel::iconForLayer( layer->layer() );

QgsLogger::warning( "Search for" + result.displayString + QStringLiteral( __FILE__ ) + ": " + QString::number( __LINE__ ) );
// return all the layers in case the string query is empty using an equal default score
if ( context.usingPrefix && string.isEmpty() )
{
QgsLogger::warning( "Using prefix but empty" + QStringLiteral( __FILE__ ) + ": " + QString::number( __LINE__ ) );
emit resultFetched( result );
continue;
}

result.score = fuzzyScore( result.displayString, string );
QgsLogger::warning( "scored: " + QString::number( result.score ) + QStringLiteral( __FILE__ ) + ": " + QString::number( __LINE__ ) );
result.score = QgsStringUtils::fuzzyScore( result.displayString, string );

if ( result.score > 0 )
{
emit resultFetched( result );
continue;
}
}
}

Expand Down Expand Up @@ -102,15 +96,24 @@ void QgsLayoutLocatorFilter::fetchResults( const QString &string, const QgsLocat
const QList< QgsMasterLayoutInterface * > layouts = QgsProject::instance()->layoutManager()->layouts();
for ( QgsMasterLayoutInterface *layout : layouts )
{
if ( layout && ( stringMatches( layout->name(), string ) || ( context.usingPrefix && string.isEmpty() ) ) )
// if the layout is broken, don't include it in the results
if ( ! layout )
continue;

QgsLocatorResult result;
result.displayString = layout->name();
result.userData = layout->name();

if ( context.usingPrefix && string.isEmpty() )
{
QgsLocatorResult result;
result.displayString = layout->name();
result.userData = layout->name();
//result.icon = QgsMapLayerModel::iconForLayer( layer->layer() );
result.score = static_cast< double >( string.length() ) / layout->name().length();
emit resultFetched( result );
continue;
}

result.score = QgsStringUtils::fuzzyScore( result.displayString, string );

if ( result.score > 0 )
emit resultFetched( result );
}
}

Expand Down Expand Up @@ -139,7 +142,7 @@ QgsActionLocatorFilter *QgsActionLocatorFilter::clone() const
return new QgsActionLocatorFilter( mActionParents );
}

void QgsActionLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback * )
void QgsActionLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &context, QgsFeedback * )
{
// collect results in main thread, since this method is inexpensive and
// accessing the gui actions is not thread safe
Expand All @@ -148,7 +151,7 @@ void QgsActionLocatorFilter::fetchResults( const QString &string, const QgsLocat

for ( QWidget *object : qgis::as_const( mActionParents ) )
{
searchActions( string, object, found );
searchActions( string, object, found );
}
}

Expand Down Expand Up @@ -208,15 +211,17 @@ void QgsActionLocatorFilter::searchActions( const QString &string, QWidget *pare
searchText += QStringLiteral( " (%1)" ).arg( tooltip.trimmed() );
}

if ( stringMatches( searchText, string ) )
QgsLocatorResult result;
result.displayString = searchText;
result.userData = QVariant::fromValue( action );
result.icon = action->icon();
result.score = fuzzyScore( result.displayString, string );

if ( result.score > 0 )
{
QgsLocatorResult result;
result.displayString = searchText;
result.userData = QVariant::fromValue( action );
result.icon = action->icon();
result.score = static_cast< double >( string.length() ) / searchText.length();
emit resultFetched( result );
found << action;
emit resultFetched( result );

}
}
}
Expand Down Expand Up @@ -541,30 +546,21 @@ void QgsSettingsLocatorFilter::fetchResults( const QString &string, const QgsLoc
for ( auto optionsPagesIterator = optionsPagesMap.constBegin(); optionsPagesIterator != optionsPagesMap.constEnd(); ++optionsPagesIterator )
{
QString title = optionsPagesIterator.key();
if ( stringMatches( title, string ) || ( context.usingPrefix && string.isEmpty() ) )
{
matchingSettingsPagesMap.insert( title + " (" + tr( "Options" ) + ")", settingsPage( QStringLiteral( "optionpage" ), QString::number( optionsPagesIterator.value() ) ) );
}
matchingSettingsPagesMap.insert( title + " (" + tr( "Options" ) + ")", settingsPage( QStringLiteral( "optionpage" ), QString::number( optionsPagesIterator.value() ) ) );
}

QMap<QString, QString> projectPropertyPagesMap = QgisApp::instance()->projectPropertiesPagesMap();
for ( auto projectPropertyPagesIterator = projectPropertyPagesMap.constBegin(); projectPropertyPagesIterator != projectPropertyPagesMap.constEnd(); ++projectPropertyPagesIterator )
{
QString title = projectPropertyPagesIterator.key();
if ( stringMatches( title, string ) || ( context.usingPrefix && string.isEmpty() ) )
{
matchingSettingsPagesMap.insert( title + " (" + tr( "Project Properties" ) + ")", settingsPage( QStringLiteral( "projectpropertypage" ), projectPropertyPagesIterator.value() ) );
}
matchingSettingsPagesMap.insert( title + " (" + tr( "Project Properties" ) + ")", settingsPage( QStringLiteral( "projectpropertypage" ), projectPropertyPagesIterator.value() ) );
}

QMap<QString, QString> settingPagesMap = QgisApp::instance()->settingPagesMap();
for ( auto settingPagesIterator = settingPagesMap.constBegin(); settingPagesIterator != settingPagesMap.constEnd(); ++settingPagesIterator )
{
QString title = settingPagesIterator.key();
if ( stringMatches( title, string ) || ( context.usingPrefix && string.isEmpty() ) )
{
matchingSettingsPagesMap.insert( title, settingsPage( QStringLiteral( "settingspage" ), settingPagesIterator.value() ) );
}
matchingSettingsPagesMap.insert( title, settingsPage( QStringLiteral( "settingspage" ), settingPagesIterator.value() ) );
}

for ( auto matchingSettingsPagesIterator = matchingSettingsPagesMap.constBegin(); matchingSettingsPagesIterator != matchingSettingsPagesMap.constEnd(); ++matchingSettingsPagesIterator )
Expand All @@ -575,8 +571,17 @@ void QgsSettingsLocatorFilter::fetchResults( const QString &string, const QgsLoc
result.filter = this;
result.displayString = title;
result.userData.setValue( settingsPage );
result.score = static_cast< double >( string.length() ) / title.length();
emit resultFetched( result );

if ( context.usingPrefix && string.isEmpty() )
{
emit resultFetched( result );
continue;
}

result.score = QgsStringUtils::fuzzyScore( result.displayString, string );;

if ( result.score > 0 )
emit resultFetched( result );
}
}

Expand Down Expand Up @@ -631,22 +636,28 @@ void QgsBookmarkLocatorFilter::fetchResults( const QString &string, const QgsLoc
while ( i.hasNext() )
{
i.next();

if ( feedback->isCanceled() )
return;

QString name = i.key();
QModelIndex index = i.value();
QgsLocatorResult result;
result.filter = this;
result.displayString = name;
result.userData = index;
result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/mItemBookmark.svg" ) );

if ( stringMatches( name, string ) || ( context.usingPrefix && string.isEmpty() ) )
if ( context.usingPrefix && string.isEmpty() )
{
QModelIndex index = i.value();
QgsLocatorResult result;
result.filter = this;
result.displayString = name;
result.userData = index;
result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/mItemBookmark.svg" ) );
result.score = static_cast< double >( string.length() ) / name.length();
emit resultFetched( result );
continue;
}

result.score = QgsStringUtils::fuzzyScore( result.displayString, string );

if ( result.score > 0 )
emit resultFetched( result );
}
}

Expand Down
9 changes: 2 additions & 7 deletions src/core/locator/qgslocatorfilter.cpp
Expand Up @@ -23,11 +23,6 @@
#include "qgsmessagelog.h"


#define FUZZY_SCORE_WORD_MATCH 5
#define FUZZY_SCORE_NEW_MATCH 3
#define FUZZY_SCORE_CONSECUTIVE_MATCH 4


QgsLocatorFilter::QgsLocatorFilter( QObject *parent )
: QObject( parent )
{
Expand Down Expand Up @@ -69,7 +64,7 @@ double QgsLocatorFilter::fuzzyScore( const QString &candidate, const QString &se
bool isPreviousIndexMatching = false;
bool isWordOpen = true;

// loop throught each candidate char and calculate the potential max score
// loop through each candidate char and calculate the potential max score
while ( candidateIdx < candidateLength )
{
QChar candidateChar = candidateNormalized[ candidateIdx++ ];
Expand All @@ -84,7 +79,7 @@ double QgsLocatorFilter::fuzzyScore( const QString &candidate, const QString &se
else
maxScore += FUZZY_SCORE_CONSECUTIVE_MATCH;

// we looped through all the characters
// we looped all the characters
if ( searchIdx >= searchLength )
continue;

Expand Down

0 comments on commit 9e75e3b

Please sign in to comment.