1 /***************************************************************************
2                         qgsalllayersfeatureslocatorfilters.cpp
3                         ----------------------------
4    begin                : May 2017
5    copyright            : (C) 2017 by Nyall Dawson
6    email                : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include "qgsalllayersfeatureslocatorfilter.h"
19 #include "qgssettings.h"
20 #include "qgsproject.h"
21 #include "qgsvectorlayer.h"
22 #include "qgsexpressioncontextutils.h"
23 #include "qgsfeatureaction.h"
24 #include "qgsfeedback.h"
25 #include "qgsiconutils.h"
26 #include "qgisapp.h"
27 #include "qgsmapcanvas.h"
28 
29 #include <QSpinBox>
30 
QgsAllLayersFeaturesLocatorFilter(QObject * parent)31 QgsAllLayersFeaturesLocatorFilter::QgsAllLayersFeaturesLocatorFilter( QObject *parent )
32   : QgsLocatorFilter( parent )
33 {
34   setUseWithoutPrefix( false );
35 }
36 
clone() const37 QgsAllLayersFeaturesLocatorFilter *QgsAllLayersFeaturesLocatorFilter::clone() const
38 {
39   return new QgsAllLayersFeaturesLocatorFilter();
40 }
41 
prepare(const QString & string,const QgsLocatorContext & context)42 QStringList QgsAllLayersFeaturesLocatorFilter::prepare( const QString &string, const QgsLocatorContext &context )
43 {
44   // Normally skip very short search strings, unless when specifically searching using this filter
45   if ( string.length() < 3 && !context.usingPrefix )
46     return QStringList();
47 
48   QgsSettings settings;
49   mMaxTotalResults = settings.value( "locator_filters/all_layers_features/limit_global", 15, QgsSettings::App ).toInt();
50   mMaxResultsPerLayer = settings.value( "locator_filters/all_layers_features/limit_per_layer", 8, QgsSettings::App ).toInt();
51 
52   mPreparedLayers.clear();
53   const QMap<QString, QgsMapLayer *> layers = QgsProject::instance()->mapLayers();
54   for ( auto it = layers.constBegin(); it != layers.constEnd(); ++it )
55   {
56     QgsVectorLayer *layer = qobject_cast< QgsVectorLayer *>( it.value() );
57     if ( !layer || !layer->dataProvider() || !layer->flags().testFlag( QgsMapLayer::Searchable ) )
58       continue;
59 
60     QgsExpression expression( layer->displayExpression() );
61     QgsExpressionContext context;
62     context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) );
63     expression.prepare( &context );
64 
65     QgsFeatureRequest req;
66     req.setSubsetOfAttributes( qgis::setToList( expression.referencedAttributeIndexes( layer->fields() ) ) );
67     if ( !expression.needsGeometry() )
68       req.setFlags( QgsFeatureRequest::NoGeometry );
69     QString enhancedSearch = string;
70     enhancedSearch.replace( ' ', '%' );
71     req.setFilterExpression( QStringLiteral( "%1 ILIKE '%%2%'" )
72                              .arg( layer->displayExpression(), enhancedSearch ) );
73     req.setLimit( mMaxResultsPerLayer );
74 
75     QgsFeatureRequest exactMatchRequest = req;
76     exactMatchRequest.setFilterExpression( QStringLiteral( "%1 ILIKE '%2'" )
77                                            .arg( layer->displayExpression(), enhancedSearch ) );
78     exactMatchRequest.setLimit( mMaxResultsPerLayer );
79 
80     std::shared_ptr<PreparedLayer> preparedLayer( new PreparedLayer() );
81     preparedLayer->expression = expression;
82     preparedLayer->context = context;
83     preparedLayer->layerId = layer->id();
84     preparedLayer->layerName = layer->name();
85     preparedLayer->featureSource.reset( new QgsVectorLayerFeatureSource( layer ) );
86     preparedLayer->request = req;
87     preparedLayer->exactMatchRequest = exactMatchRequest;
88     preparedLayer->layerIcon = QgsIconUtils::iconForLayer( layer );
89     preparedLayer->layerIsSpatial = layer->isSpatial();
90 
91     mPreparedLayers.append( preparedLayer );
92   }
93 
94   return QStringList();
95 }
96 
fetchResults(const QString & string,const QgsLocatorContext &,QgsFeedback * feedback)97 void QgsAllLayersFeaturesLocatorFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
98 {
99   int foundInCurrentLayer;
100   int foundInTotal = 0;
101   QgsFeature f;
102 
103   // we cannot used const loop since iterator::nextFeature is not const
104   for ( auto preparedLayer : std::as_const( mPreparedLayers ) )
105   {
106     foundInCurrentLayer = 0;
107 
108     QgsFeatureIds foundFeatureIds;
109 
110     QgsFeatureIterator exactMatchIt = preparedLayer->featureSource->getFeatures( preparedLayer->exactMatchRequest );
111     while ( exactMatchIt.nextFeature( f ) )
112     {
113       if ( feedback->isCanceled() )
114         return;
115 
116       QgsLocatorResult result;
117       result.group = preparedLayer->layerName;
118 
119       preparedLayer->context.setFeature( f );
120 
121       result.displayString = preparedLayer->expression.evaluate( &( preparedLayer->context ) ).toString();
122 
123       result.userData = ResultData( f.id(), preparedLayer->layerId, preparedLayer->layerIsSpatial ).toVariant();
124       foundFeatureIds << f.id();
125       result.icon = preparedLayer->layerIcon;
126       result.score = static_cast< double >( string.length() ) / result.displayString.size();
127 
128       result.actions << QgsLocatorResult::ResultAction( OpenForm, tr( "Open form…" ) );
129       emit resultFetched( result );
130 
131       foundInCurrentLayer++;
132       foundInTotal++;
133       if ( foundInCurrentLayer >= mMaxResultsPerLayer )
134         break;
135     }
136     if ( foundInCurrentLayer >= mMaxResultsPerLayer )
137       continue;
138     if ( foundInTotal >= mMaxTotalResults )
139       break;
140 
141     QgsFeatureIterator it = preparedLayer->featureSource->getFeatures( preparedLayer->request );
142     while ( it.nextFeature( f ) )
143     {
144       if ( feedback->isCanceled() )
145         return;
146 
147       if ( foundFeatureIds.contains( f.id() ) )
148         continue;
149 
150       QgsLocatorResult result;
151       result.group = preparedLayer->layerName;
152 
153       preparedLayer->context.setFeature( f );
154 
155       result.displayString = preparedLayer->expression.evaluate( &( preparedLayer->context ) ).toString();
156 
157       result.userData = ResultData( f.id(), preparedLayer->layerId, preparedLayer->layerIsSpatial ).toVariant();
158       result.icon = preparedLayer->layerIcon;
159       result.score = static_cast< double >( string.length() ) / result.displayString.size();
160 
161       if ( preparedLayer->layerIsSpatial )
162         result.actions << QgsLocatorResult::ResultAction( OpenForm, tr( "Open form…" ) );
163       emit resultFetched( result );
164 
165       foundInCurrentLayer++;
166       foundInTotal++;
167       if ( foundInCurrentLayer >= mMaxResultsPerLayer )
168         break;
169     }
170     if ( foundInTotal >= mMaxTotalResults )
171       break;
172   }
173 }
174 
triggerResult(const QgsLocatorResult & result)175 void QgsAllLayersFeaturesLocatorFilter::triggerResult( const QgsLocatorResult &result )
176 {
177   triggerResultFromAction( result, NoEntry );
178 }
179 
triggerResultFromAction(const QgsLocatorResult & result,const int actionId)180 void QgsAllLayersFeaturesLocatorFilter::triggerResultFromAction( const QgsLocatorResult &result, const int actionId )
181 {
182   ResultData data = ResultData::fromVariant( result.userData );
183   QgsFeatureId fid = data.id();
184   QString layerId = data.layerId();
185   bool layerIsSpatial = data.layerIsSpatial();
186   QgsVectorLayer *layer = QgsProject::instance()->mapLayer<QgsVectorLayer *>( layerId );
187   if ( !layer )
188     return;
189 
190   if ( actionId == OpenForm || !layerIsSpatial )
191   {
192     QgsFeature f;
193     QgsFeatureRequest request;
194     request.setFilterFid( fid );
195     bool fetched = layer->getFeatures( request ).nextFeature( f );
196     if ( !fetched )
197       return;
198     QgsFeatureAction action( tr( "Attributes changed" ), f, layer, QString(), -1, QgisApp::instance() );
199     if ( layer->isEditable() )
200     {
201       action.editFeature( false );
202     }
203     else
204     {
205       action.viewFeatureForm();
206     }
207   }
208   else
209   {
210     QgisApp::instance()->mapCanvas()->zoomToFeatureIds( layer, QgsFeatureIds() << fid );
211     QgisApp::instance()->mapCanvas()->flashFeatureIds( layer, QgsFeatureIds() << fid );
212   }
213 }
214 
openConfigWidget(QWidget * parent)215 void QgsAllLayersFeaturesLocatorFilter::openConfigWidget( QWidget *parent )
216 {
217   QString key = "locator_filters/all_layers_features";
218   QgsSettings settings;
219   std::unique_ptr<QDialog> dlg( new QDialog( parent ) );
220   dlg->restoreGeometry( settings.value( QStringLiteral( "Windows/%1/geometry" ).arg( key ) ).toByteArray() );
221   dlg->setWindowTitle( "All layers features locator filter" );
222   QFormLayout *formLayout = new QFormLayout;
223   QSpinBox *globalLimitSpinBox = new QSpinBox( dlg.get() );
224   globalLimitSpinBox->setValue( settings.value( QStringLiteral( "%1/limit_global" ).arg( key ), 15, QgsSettings::App ).toInt() );
225   globalLimitSpinBox->setMinimum( 1 );
226   globalLimitSpinBox->setMaximum( 200 );
227   formLayout->addRow( tr( "&Maximum number of results:" ), globalLimitSpinBox );
228   QSpinBox *perLayerLimitSpinBox = new QSpinBox( dlg.get() );
229   perLayerLimitSpinBox->setValue( settings.value( QStringLiteral( "%1/limit_per_layer" ).arg( key ), 8, QgsSettings::App ).toInt() );
230   perLayerLimitSpinBox->setMinimum( 1 );
231   perLayerLimitSpinBox->setMaximum( 200 );
232   formLayout->addRow( tr( "&Maximum number of results per layer:" ), perLayerLimitSpinBox );
233   QDialogButtonBox *buttonbBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dlg.get() );
234   formLayout->addRow( buttonbBox );
235   dlg->setLayout( formLayout );
236   connect( buttonbBox, &QDialogButtonBox::accepted, [&]()
237   {
238     settings.setValue( QStringLiteral( "%1/limit_global" ).arg( key ), globalLimitSpinBox->value(), QgsSettings::App );
239     settings.setValue( QStringLiteral( "%1/limit_per_layer" ).arg( key ), perLayerLimitSpinBox->value(), QgsSettings::App );
240     dlg->accept();
241   } );
242   connect( buttonbBox, &QDialogButtonBox::rejected, dlg.get(), &QDialog::reject );
243   dlg->exec();
244 }
245