Navigation Menu

Skip to content

Commit

Permalink
Merge pull request #38633 from 3nids/locator-autocomplete
Browse files Browse the repository at this point in the history
[locator] autocomplete + allow restricting search to a single field in active layer filter
  • Loading branch information
3nids committed Sep 9, 2020
2 parents ce9edb6 + 950fda6 commit d6666c0
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 32 deletions.
16 changes: 16 additions & 0 deletions python/core/auto_generated/locator/qgslocator.sip.in
Expand Up @@ -131,6 +131,14 @@ Returns ``True`` if a query is currently being executed by the locator.
Will call clearPreviousResults on all filters

.. versionadded:: 3.2
%End

QStringList completionList() const;
%Docstring
Returns the list for auto completion
This list is updated when preparing the search

.. versionadded:: 3.16
%End

signals:
Expand All @@ -139,6 +147,14 @@ Will call clearPreviousResults on all filters
%Docstring
Emitted whenever a filter encounters a matching ``result`` after the :py:func:`~QgsLocator.fetchResults` method
is called.
%End

void searchPrepared();
%Docstring
Emitted when locator has prepared the search (:py:func:`QgsLocatorFilter.prepare`)
before the search is actually performed

.. versionadded:: 3.16
%End

void finished();
Expand Down
3 changes: 2 additions & 1 deletion python/core/auto_generated/locator/qgslocatorfilter.sip.in
Expand Up @@ -163,12 +163,13 @@ results from this filter.
.. seealso:: :py:func:`activePrefix`
%End

virtual void prepare( const QString &string, const QgsLocatorContext &context );
virtual QStringList prepare( const QString &string, const QgsLocatorContext &context );
%Docstring
Prepares the filter instance for an upcoming search for the specified ``string``. This method is always called
from the main thread, and individual filter subclasses should perform whatever
tasks are required in order to allow a subsequent search to safely execute
on a background thread.
The method returns an autocompletion list
%End

virtual void fetchResults( const QString &string, const QgsLocatorContext &context, QgsFeedback *feedback ) = 0;
Expand Down
1 change: 1 addition & 0 deletions python/gui/auto_generated/locator/qgslocatorwidget.sip.in
Expand Up @@ -10,6 +10,7 @@




class QgsLocatorWidget : QWidget
{
%Docstring
Expand Down
66 changes: 49 additions & 17 deletions src/app/locator/qgsinbuiltlocatorfilters.cpp
Expand Up @@ -244,35 +244,58 @@ QgsActiveLayerFeaturesLocatorFilter *QgsActiveLayerFeaturesLocatorFilter::clone(
return new QgsActiveLayerFeaturesLocatorFilter();
}

void QgsActiveLayerFeaturesLocatorFilter::prepare( const QString &string, const QgsLocatorContext &context )
QString QgsActiveLayerFeaturesLocatorFilter::fieldRestriction( QString &searchString ) const
{
QString _fieldRestriction;
if ( searchString.startsWith( '@' ) )
{
_fieldRestriction = searchString.left( std::max( searchString.indexOf( ' ' ), 0 ) ).remove( 0, 1 );
searchString = searchString.mid( _fieldRestriction.length() + 2 );
}
return _fieldRestriction;
}

QStringList QgsActiveLayerFeaturesLocatorFilter::prepare( const QString &string, const QgsLocatorContext &context )
{
// Normally skip very short search strings, unless when specifically searching using this filter
if ( string.length() < 3 && !context.usingPrefix )
return;
return QStringList();

QgsSettings settings;
mMaxTotalResults = settings.value( QStringLiteral( "locator_filters/active_layer_features/limit_global" ), 30, QgsSettings::App ).toInt();

bool allowNumeric = false;
double numericalValue = string.toDouble( &allowNumeric );

QgsVectorLayer *layer = qobject_cast< QgsVectorLayer *>( QgisApp::instance()->activeLayer() );
if ( !layer )
return;
return QStringList();

mDispExpression = QgsExpression( layer->displayExpression() );
mContext.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) );
mDispExpression.prepare( &mContext );

// determine if search is restricted to a specific field
QString searchString = string;
QString _fieldRestriction = fieldRestriction( searchString );
bool allowNumeric = false;
double numericalValue = searchString.toDouble( &allowNumeric );

// build up request expression
QStringList expressionParts;
QStringList completionList;
const QgsFields fields = layer->fields();
QgsAttributeList subsetOfAttributes;
for ( const QgsField &field : fields )
{
if ( !_fieldRestriction.isEmpty() && !field.name().startsWith( _fieldRestriction ) )
continue;

if ( !_fieldRestriction.isEmpty() )
subsetOfAttributes << layer->fields().indexFromName( field.name() );

completionList.append( QStringLiteral( "@%1 " ).arg( field.name() ) );
if ( field.type() == QVariant::String )
{
expressionParts << QStringLiteral( "%1 ILIKE '%%2%'" ).arg( QgsExpression::quotedColumnRef( field.name() ),
string );
searchString );
}
else if ( allowNumeric && field.isNumeric() )
{
Expand All @@ -285,6 +308,9 @@ void QgsActiveLayerFeaturesLocatorFilter::prepare( const QString &string, const
QgsFeatureRequest req;
req.setFlags( QgsFeatureRequest::NoGeometry );
req.setFilterExpression( expression );
if ( !_fieldRestriction.isEmpty() )
req.setSubsetOfAttributes( subsetOfAttributes );

req.setLimit( 30 );
mIterator = layer->getFeatures( req );

Expand All @@ -295,12 +321,16 @@ void QgsActiveLayerFeaturesLocatorFilter::prepare( const QString &string, const
{
mAttributeAliases.append( layer->attributeDisplayName( idx ) );
}

return completionList;
}

void QgsActiveLayerFeaturesLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
{
int found = 0;
QgsFeature f;
QString searchString = string;
fieldRestriction( searchString );

while ( mIterator.nextFeature( f ) )
{
Expand All @@ -317,7 +347,7 @@ void QgsActiveLayerFeaturesLocatorFilter::fetchResults( const QString &string, c
for ( const QVariant &var : attributes )
{
QString attrString = var.toString();
if ( attrString.contains( string, Qt::CaseInsensitive ) )
if ( attrString.contains( searchString, Qt::CaseInsensitive ) )
{
if ( idx < mAttributeAliases.count() )
result.displayString = QStringLiteral( "%1 (%2)" ).arg( attrString, mAttributeAliases[idx] );
Expand All @@ -334,7 +364,7 @@ void QgsActiveLayerFeaturesLocatorFilter::fetchResults( const QString &string, c

result.userData = QVariantList() << f.id() << mLayerId;
result.icon = mLayerIcon;
result.score = static_cast< double >( string.length() ) / result.displayString.size();
result.score = static_cast< double >( searchString.length() ) / result.displayString.size();
emit resultFetched( result );

found++;
Expand Down Expand Up @@ -395,11 +425,11 @@ QgsAllLayersFeaturesLocatorFilter *QgsAllLayersFeaturesLocatorFilter::clone() co
return new QgsAllLayersFeaturesLocatorFilter();
}

void QgsAllLayersFeaturesLocatorFilter::prepare( const QString &string, const QgsLocatorContext &context )
QStringList QgsAllLayersFeaturesLocatorFilter::prepare( const QString &string, const QgsLocatorContext &context )
{
// Normally skip very short search strings, unless when specifically searching using this filter
if ( string.length() < 3 && !context.usingPrefix )
return;
return QStringList();

QgsSettings settings;
mMaxTotalResults = settings.value( "locator_filters/all_layers_features/limit_global", 15, QgsSettings::App ).toInt();
Expand Down Expand Up @@ -445,6 +475,8 @@ void QgsAllLayersFeaturesLocatorFilter::prepare( const QString &string, const Qg

mPreparedLayers.append( preparedLayer );
}

return QStringList();
}

void QgsAllLayersFeaturesLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
Expand Down Expand Up @@ -573,18 +605,18 @@ void QgsAllLayersFeaturesLocatorFilter::openConfigWidget( QWidget *parent )
globalLimitSpinBox->setMinimum( 1 );
globalLimitSpinBox->setMaximum( 200 );
formLayout->addRow( tr( "&Maximum number of results:" ), globalLimitSpinBox );
QSpinBox *parLayerLimitSpinBox = new QSpinBox( dlg.get() );
parLayerLimitSpinBox->setValue( settings.value( QStringLiteral( "%1/limit_per_layer" ).arg( key ), 8, QgsSettings::App ).toInt() );
parLayerLimitSpinBox->setMinimum( 1 );
parLayerLimitSpinBox->setMaximum( 200 );
formLayout->addRow( tr( "&Maximum number of results per layer:" ), parLayerLimitSpinBox );
QSpinBox *perLayerLimitSpinBox = new QSpinBox( dlg.get() );
perLayerLimitSpinBox->setValue( settings.value( QStringLiteral( "%1/limit_per_layer" ).arg( key ), 8, QgsSettings::App ).toInt() );
perLayerLimitSpinBox->setMinimum( 1 );
perLayerLimitSpinBox->setMaximum( 200 );
formLayout->addRow( tr( "&Maximum number of results per layer:" ), perLayerLimitSpinBox );
QDialogButtonBox *buttonbBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dlg.get() );
formLayout->addRow( buttonbBox );
dlg->setLayout( formLayout );
connect( buttonbBox, &QDialogButtonBox::accepted, [&]()
{
settings.setValue( QStringLiteral( "%1/limit_global" ).arg( key ), globalLimitSpinBox->value(), QgsSettings::App );
settings.setValue( QStringLiteral( "%1/limit_per_layer" ).arg( key ), parLayerLimitSpinBox->value(), QgsSettings::App );
settings.setValue( QStringLiteral( "%1/limit_per_layer" ).arg( key ), perLayerLimitSpinBox->value(), QgsSettings::App );
dlg->accept();
} );
connect( buttonbBox, &QDialogButtonBox::rejected, dlg.get(), &QDialog::reject );
Expand Down
10 changes: 8 additions & 2 deletions src/app/locator/qgsinbuiltlocatorfilters.h
Expand Up @@ -102,14 +102,20 @@ class APP_EXPORT QgsActiveLayerFeaturesLocatorFilter : public QgsLocatorFilter
Priority priority() const override { return Medium; }
QString prefix() const override { return QStringLiteral( "f" ); }

void prepare( const QString &string, const QgsLocatorContext &context ) override;
QStringList prepare( const QString &string, const QgsLocatorContext &context ) override;
void fetchResults( const QString &string, const QgsLocatorContext &context, QgsFeedback *feedback ) override;
void triggerResult( const QgsLocatorResult &result ) override;
bool hasConfigWidget() const override {return true;}
void openConfigWidget( QWidget *parent ) override;

private:

/**
* Returns the field restriction if defined (starting with @)
* The \a searchString is modified accordingly by removing the field restriction
*/
QString fieldRestriction( QString &searchString ) const;

QgsExpression mDispExpression;
QgsExpressionContext mContext;
QgsFeatureIterator mIterator;
Expand Down Expand Up @@ -150,7 +156,7 @@ class APP_EXPORT QgsAllLayersFeaturesLocatorFilter : public QgsLocatorFilter
Priority priority() const override { return Medium; }
QString prefix() const override { return QStringLiteral( "af" ); }

void prepare( const QString &string, const QgsLocatorContext &context ) override;
QStringList prepare( const QString &string, const QgsLocatorContext &context ) override;
void fetchResults( const QString &string, const QgsLocatorContext &context, QgsFeedback *feedback ) override;
void triggerResult( const QgsLocatorResult &result ) override;
void triggerResultFromAction( const QgsLocatorResult &result, const int actionId ) override;
Expand Down
15 changes: 13 additions & 2 deletions src/core/locator/qgslocator.cpp
Expand Up @@ -124,6 +124,8 @@ void QgsLocator::registerFilter( QgsLocatorFilter *filter )

void QgsLocator::fetchResults( const QString &string, const QgsLocatorContext &c, QgsFeedback *feedback )
{
mAutocompletionList.clear();

QgsLocatorContext context( c );
// ideally this should not be required, as well behaved callers
// will NOT fire up a new fetchResults call while an existing one is
Expand Down Expand Up @@ -182,7 +184,15 @@ void QgsLocator::fetchResults( const QString &string, const QgsLocatorContext &c
result.filter = filter;
filterSentResult( result );
} );
clone->prepare( searchString, context );
QStringList autoCompleteList = clone->prepare( searchString, context );
if ( context.usingPrefix )
{
for ( int i = 0; i < autoCompleteList.length(); i++ )
{
autoCompleteList[i].prepend( QStringLiteral( "%1 " ).arg( prefix ) );
}
}
mAutocompletionList.append( autoCompleteList );

if ( clone->flags() & QgsLocatorFilter::FlagFast )
{
Expand All @@ -191,7 +201,6 @@ void QgsLocator::fetchResults( const QString &string, const QgsLocatorContext &c
}
else
{
// run filter in background
threadedFilters.append( clone.release() );
}
}
Expand Down Expand Up @@ -220,6 +229,8 @@ void QgsLocator::fetchResults( const QString &string, const QgsLocatorContext &c
thread->start();
}

emit searchPrepared();

if ( mActiveThreads.empty() )
emit finished();
}
Expand Down
16 changes: 16 additions & 0 deletions src/core/locator/qgslocator.h
Expand Up @@ -145,6 +145,13 @@ class CORE_EXPORT QgsLocator : public QObject
*/
void clearPreviousResults();

/**
* Returns the list for auto completion
* This list is updated when preparing the search
* \since QGIS 3.16
*/
QStringList completionList() const {return mAutocompletionList;}

signals:

/**
Expand All @@ -153,6 +160,13 @@ class CORE_EXPORT QgsLocator : public QObject
*/
void foundResult( const QgsLocatorResult &result );

/**
* Emitted when locator has prepared the search (\see QgsLocatorFilter::prepare)
* before the search is actually performed
* \since QGIS 3.16
*/
void searchPrepared();

/**
* Emitted when locator has finished a query, either as a result
* of successful completion or early cancellation.
Expand All @@ -171,6 +185,8 @@ class CORE_EXPORT QgsLocator : public QObject
QList< QgsLocatorFilter * > mFilters;
QList< QThread * > mActiveThreads;

QStringList mAutocompletionList;

void cancelRunningQuery();

};
Expand Down
3 changes: 2 additions & 1 deletion src/core/locator/qgslocatorfilter.h
Expand Up @@ -219,8 +219,9 @@ class CORE_EXPORT QgsLocatorFilter : public QObject
* from the main thread, and individual filter subclasses should perform whatever
* tasks are required in order to allow a subsequent search to safely execute
* on a background thread.
* The method returns an autocompletion list
*/
virtual void prepare( const QString &string, const QgsLocatorContext &context ) { Q_UNUSED( string ) Q_UNUSED( context ); }
virtual QStringList prepare( const QString &string, const QgsLocatorContext &context ) { Q_UNUSED( string ) Q_UNUSED( context ); return QStringList();}

/**
* Retrieves the filter results for a specified search \a string. The \a context
Expand Down

0 comments on commit d6666c0

Please sign in to comment.