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