Skip to content

Commit 0bfc9bb

Browse files
committedDec 22, 2016
[FEATURE] Show field values in autocompleter in form filter mode
This adds a new gui widget QgsFieldValuesLineEdit which includes an autocompleter populated with current field values. The autocompleter is nicely updated in the background so that the gui remains nice and responsive, even if there's millions of records in the associated table. It's now used as a search widget for text fields, so can be seen in the browser window if you set the filter to a text field, or if you launch the form based select/filter by selecting a layer and pressing F3.
1 parent e27822b commit 0bfc9bb

File tree

7 files changed

+430
-3
lines changed

7 files changed

+430
-3
lines changed
 

‎python/gui/gui.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
%Include qgsfieldmodel.sip
7676
%Include qgsfieldproxymodel.sip
7777
%Include qgsfieldvalidator.sip
78+
%Include qgsfieldvalueslineedit.sip
7879
%Include qgsfiledropedit.sip
7980
%Include qgsfilewidget.sip
8081
%Include qgsfiledownloader.sip

‎python/gui/qgsfieldvalueslineedit.sip

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/** \class QgsFieldValuesLineEdit
2+
* \ingroup gui
3+
* A line edit with an autocompleter which takes unique values from a vector layer's fields.
4+
* The autocompleter is populated from the vector layer in the background to ensure responsive
5+
* interaction with the widget.
6+
* \note added in QGIS 3.0
7+
*/
8+
class QgsFieldValuesLineEdit: QgsFilterLineEdit
9+
{
10+
%TypeHeaderCode
11+
#include <qgsfieldvalueslineedit.h>
12+
%End
13+
public:
14+
15+
/** Constructor for QgsFieldValuesLineEdit
16+
* @param parent parent widget
17+
*/
18+
QgsFieldValuesLineEdit( QWidget *parent /TransferThis/ = nullptr );
19+
20+
virtual ~QgsFieldValuesLineEdit();
21+
22+
/** Sets the layer containing the field that values will be shown from.
23+
* @param layer vector layer
24+
* @see layer()
25+
* @see setAttributeIndex()
26+
*/
27+
void setLayer( QgsVectorLayer* layer );
28+
29+
/** Returns the layer containing the field that values will be shown from.
30+
* @see setLayer()
31+
* @see attributeIndex()
32+
*/
33+
QgsVectorLayer* layer() const;
34+
35+
/** Sets the attribute index for the field containing values to show in the widget.
36+
* @param index index of attribute
37+
* @see attributeIndex()
38+
* @see setLayer()
39+
*/
40+
void setAttributeIndex( int index );
41+
42+
/** Returns the attribute index for the field containing values shown in the widget.
43+
* @see setAttributeIndex()
44+
* @see layer()
45+
*/
46+
int attributeIndex() const;
47+
48+
signals:
49+
50+
/** Emitted when the layer associated with the widget changes.
51+
* @param layer vector layer
52+
*/
53+
void layerChanged( QgsVectorLayer* layer );
54+
55+
/** Emitted when the field associated with the widget changes.
56+
* @param index new attribute index for field
57+
*/
58+
void attributeIndexChanged( int index );
59+
};

‎src/gui/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ SET(QGIS_GUI_SRCS
223223
qgsfieldmodel.cpp
224224
qgsfieldproxymodel.cpp
225225
qgsfieldvalidator.cpp
226+
qgsfieldvalueslineedit.cpp
226227
qgsfiledropedit.cpp
227228
qgsfilewidget.cpp
228229
qgsfilterlineedit.cpp
@@ -391,6 +392,7 @@ SET(QGIS_GUI_MOC_HDRS
391392
qgsfieldmodel.h
392393
qgsfieldproxymodel.h
393394
qgsfieldvalidator.h
395+
qgsfieldvalueslineedit.h
394396
qgsfiledropedit.h
395397
qgsfilewidget.h
396398
qgsfilterlineedit.h

‎src/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "qgsfields.h"
1919
#include "qgsfieldvalidator.h"
2020
#include "qgsexpression.h"
21+
#include "qgsfieldvalueslineedit.h"
2122
#include <QSettings>
2223
#include <QHBoxLayout>
2324

@@ -255,10 +256,20 @@ void QgsDefaultSearchWidgetWrapper::initWidget( QWidget* widget )
255256
mContainer->setLayout( new QHBoxLayout() );
256257
mContainer->layout()->setMargin( 0 );
257258
mContainer->layout()->setContentsMargins( 0, 0, 0, 0 );
258-
mLineEdit = new QgsFilterLineEdit();
259+
QVariant::Type fldType = layer()->fields().at( mFieldIdx ).type();
260+
261+
if ( fldType == QVariant::String )
262+
{
263+
mLineEdit = new QgsFieldValuesLineEdit();
264+
static_cast< QgsFieldValuesLineEdit* >( mLineEdit )->setLayer( layer() );
265+
static_cast< QgsFieldValuesLineEdit* >( mLineEdit )->setAttributeIndex( mFieldIdx );
266+
}
267+
else
268+
{
269+
mLineEdit = new QgsFilterLineEdit();
270+
}
259271
mContainer->layout()->addWidget( mLineEdit );
260272

261-
QVariant::Type fldType = layer()->fields().at( mFieldIdx ).type();
262273
if ( fldType == QVariant::String )
263274
{
264275
mCheckbox = new QCheckBox( QStringLiteral( "Case sensitive" ) );

‎src/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
#define QGSDEFAULTSEARCHWIDGETWRAPPER_H
1818

1919
#include "qgssearchwidgetwrapper.h"
20-
#include <qgsfilterlineedit.h>
20+
#include "qgsfilterlineedit.h"
2121

2222
#include <QCheckBox>
2323

‎src/gui/qgsfieldvalueslineedit.cpp

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/***************************************************************************
2+
qgsfieldvalueslineedit.cpp
3+
-------------------------
4+
Date : 20-08-2016
5+
Copyright : (C) 2016 by Nyall Dawson
6+
Email : nyall dot dawson at gmail dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#include "qgsfieldvalueslineedit.h"
17+
#include "qgsvectorlayer.h"
18+
#include "qgsfloatingwidget.h"
19+
#include <QCompleter>
20+
#include <QStringListModel>
21+
#include <QTimer>
22+
#include <QHBoxLayout>
23+
24+
QgsFieldValuesLineEdit::QgsFieldValuesLineEdit( QWidget *parent )
25+
: QgsFilterLineEdit( parent )
26+
, mLayer( nullptr )
27+
, mAttributeIndex( -1 )
28+
, mUpdateRequested( false )
29+
, mGatherer( nullptr )
30+
{
31+
QCompleter* c = new QCompleter( this );
32+
c->setCaseSensitivity( Qt::CaseInsensitive );
33+
c->setFilterMode( Qt::MatchContains );
34+
setCompleter( c );
35+
connect( this, &QgsFieldValuesLineEdit::textEdited, this, &QgsFieldValuesLineEdit::requestCompleterUpdate );
36+
mShowPopupTimer.setSingleShot( true );
37+
mShowPopupTimer.setInterval( 100 );
38+
connect( &mShowPopupTimer, &QTimer::timeout, this, &QgsFieldValuesLineEdit::triggerCompleterUpdate );
39+
}
40+
41+
QgsFieldValuesLineEdit::~QgsFieldValuesLineEdit()
42+
{
43+
if ( mGatherer )
44+
{
45+
mGatherer->stop();
46+
mGatherer->wait(); // mGatherer is deleted when wait completes
47+
}
48+
}
49+
50+
void QgsFieldValuesLineEdit::setLayer( QgsVectorLayer* layer )
51+
{
52+
if ( mLayer == layer )
53+
return;
54+
55+
mLayer = layer;
56+
emit layerChanged( layer );
57+
}
58+
59+
void QgsFieldValuesLineEdit::setAttributeIndex( int index )
60+
{
61+
if ( mAttributeIndex == index )
62+
return;
63+
64+
mAttributeIndex = index;
65+
emit attributeIndexChanged( index );
66+
}
67+
68+
void QgsFieldValuesLineEdit::requestCompleterUpdate()
69+
{
70+
mUpdateRequested = true;
71+
mShowPopupTimer.start();
72+
}
73+
74+
void QgsFieldValuesLineEdit::triggerCompleterUpdate()
75+
{
76+
mShowPopupTimer.stop();
77+
QString currentText = text();
78+
79+
if ( currentText.isEmpty() )
80+
{
81+
if ( mGatherer )
82+
mGatherer->stop();
83+
return;
84+
}
85+
86+
updateCompletionList( currentText );
87+
}
88+
89+
void QgsFieldValuesLineEdit::updateCompletionList( const QString &text )
90+
{
91+
if ( text.isEmpty() )
92+
{
93+
if ( mGatherer )
94+
mGatherer->stop();
95+
return;
96+
}
97+
98+
mUpdateRequested = true;
99+
if ( mGatherer )
100+
{
101+
mRequestedCompletionText = text;
102+
mGatherer->stop();
103+
return;
104+
}
105+
106+
mGatherer = new QgsFieldValuesLineEditValuesGatherer( mLayer, mAttributeIndex );
107+
mGatherer->setSubstring( text );
108+
109+
connect( mGatherer, &QgsFieldValuesLineEditValuesGatherer::collectedValues, this, &QgsFieldValuesLineEdit::updateCompleter );
110+
connect( mGatherer, &QgsFieldValuesLineEditValuesGatherer::finished, this, &QgsFieldValuesLineEdit::gathererThreadFinished );
111+
112+
mGatherer->start();
113+
}
114+
115+
void QgsFieldValuesLineEdit::gathererThreadFinished()
116+
{
117+
bool wasCancelled = mGatherer->wasCancelled();
118+
119+
delete mGatherer;
120+
mGatherer = nullptr;
121+
122+
if ( wasCancelled )
123+
{
124+
QString text = mRequestedCompletionText;
125+
mRequestedCompletionText.clear();
126+
updateCompletionList( text );
127+
return;
128+
}
129+
}
130+
131+
void QgsFieldValuesLineEdit::updateCompleter( const QStringList& values )
132+
{
133+
mUpdateRequested = false;
134+
completer()->setModel( new QStringListModel( values ) );
135+
completer()->complete();
136+
}
137+

‎src/gui/qgsfieldvalueslineedit.h

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/***************************************************************************
2+
qgsfieldvalueslineedit.h
3+
-----------------------
4+
Date : 20-08-2016
5+
Copyright : (C) 2016 by Nyall Dawson
6+
Email : nyall dot dawson at gmail dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
#ifndef QGSFIELDVALUESLINEEDIT_H
16+
#define QGSFIELDVALUESLINEEDIT_H
17+
18+
#include "qgsfilterlineedit.h"
19+
#include "qgsfeedback.h"
20+
#include "qgsvectorlayer.h"
21+
#include <QStringListModel>
22+
#include <QTreeView>
23+
#include <QFocusEvent>
24+
#include <QHeaderView>
25+
#include <QTimer>
26+
#include <QThread>
27+
28+
class QgsFloatingWidget;
29+
30+
// just internal guff - definitely not for exposing to public API!
31+
///@cond PRIVATE
32+
33+
/** \class QgsFieldValuesLineEditValuesGatherer
34+
* Collates unique values containing a matching substring in a thread.
35+
*/
36+
class QgsFieldValuesLineEditValuesGatherer: public QThread
37+
{
38+
Q_OBJECT
39+
40+
public:
41+
QgsFieldValuesLineEditValuesGatherer( QgsVectorLayer* layer, int attributeIndex )
42+
: mLayer( layer )
43+
, mAttributeIndex( attributeIndex )
44+
, mFeedback( nullptr )
45+
, mWasCancelled( false )
46+
{}
47+
48+
/** Sets the substring to find matching values containing
49+
*/
50+
void setSubstring( const QString& string ) { mSubstring = string; }
51+
52+
virtual void run() override
53+
{
54+
mWasCancelled = false;
55+
if ( mSubstring.isEmpty() )
56+
{
57+
emit collectedValues( QStringList() );
58+
return;
59+
}
60+
61+
// allow responsive cancellation
62+
mFeedback = new QgsFeedback();
63+
// just get 100 values... maybe less/more would be useful?
64+
mValues = mLayer->uniqueStringsMatching( mAttributeIndex, mSubstring, 100, mFeedback );
65+
66+
// be overly cautious - it's *possible* stop() might be called between deleting mFeedback and nulling it
67+
mFeedbackMutex.lock();
68+
delete mFeedback;
69+
mFeedback = nullptr;
70+
mFeedbackMutex.unlock();
71+
72+
emit collectedValues( mValues );
73+
}
74+
75+
//! Informs the gatherer to immediately stop collecting values
76+
void stop()
77+
{
78+
// be cautious, in case gatherer stops naturally just as we are cancelling it and mFeedback gets deleted
79+
mFeedbackMutex.lock();
80+
if ( mFeedback )
81+
mFeedback->cancel();
82+
mFeedbackMutex.unlock();
83+
84+
mWasCancelled = true;
85+
}
86+
87+
//! Returns true if collection was cancelled before completion
88+
bool wasCancelled() const { return mWasCancelled; }
89+
90+
signals:
91+
92+
/** Emitted when values have been collected
93+
* @param values list of unique matching string values
94+
*/
95+
void collectedValues( const QStringList& values );
96+
97+
private:
98+
99+
QgsVectorLayer* mLayer;
100+
int mAttributeIndex;
101+
QString mSubstring;
102+
QStringList mValues;
103+
QgsFeedback* mFeedback;
104+
QMutex mFeedbackMutex;
105+
bool mWasCancelled;
106+
};
107+
108+
///@endcond
109+
110+
/** \class QgsFieldValuesLineEdit
111+
* \ingroup gui
112+
* A line edit with an autocompleter which takes unique values from a vector layer's fields.
113+
* The autocompleter is populated from the vector layer in the background to ensure responsive
114+
* interaction with the widget.
115+
* \note added in QGIS 3.0
116+
*/
117+
class GUI_EXPORT QgsFieldValuesLineEdit: public QgsFilterLineEdit
118+
{
119+
Q_OBJECT
120+
121+
Q_PROPERTY( QgsVectorLayer* layer READ layer WRITE setLayer NOTIFY layerChanged )
122+
Q_PROPERTY( int attributeIndex READ attributeIndex WRITE setAttributeIndex NOTIFY attributeIndexChanged )
123+
124+
public:
125+
126+
/** Constructor for QgsFieldValuesLineEdit
127+
* @param parent parent widget
128+
*/
129+
QgsFieldValuesLineEdit( QWidget *parent = nullptr );
130+
131+
virtual ~QgsFieldValuesLineEdit();
132+
133+
/** Sets the layer containing the field that values will be shown from.
134+
* @param layer vector layer
135+
* @see layer()
136+
* @see setAttributeIndex()
137+
*/
138+
void setLayer( QgsVectorLayer* layer );
139+
140+
/** Returns the layer containing the field that values will be shown from.
141+
* @see setLayer()
142+
* @see attributeIndex()
143+
*/
144+
QgsVectorLayer* layer() const { return mLayer; }
145+
146+
/** Sets the attribute index for the field containing values to show in the widget.
147+
* @param index index of attribute
148+
* @see attributeIndex()
149+
* @see setLayer()
150+
*/
151+
void setAttributeIndex( int index );
152+
153+
/** Returns the attribute index for the field containing values shown in the widget.
154+
* @see setAttributeIndex()
155+
* @see layer()
156+
*/
157+
int attributeIndex() const { return mAttributeIndex; }
158+
159+
signals:
160+
161+
/** Emitted when the layer associated with the widget changes.
162+
* @param layer vector layer
163+
*/
164+
void layerChanged( QgsVectorLayer* layer );
165+
166+
/** Emitted when the field associated with the widget changes.
167+
* @param index new attribute index for field
168+
*/
169+
void attributeIndexChanged( int index );
170+
171+
private slots:
172+
173+
/** Requests that the autocompleter updates its completion list. The update will not occur immediately
174+
* but after a preset timeout to avoid multiple updates while a user is quickly typing.
175+
*/
176+
void requestCompleterUpdate();
177+
178+
/** Updates the autocompleter list immediately. Calling
179+
* this will trigger a background request to the layer to fetch matching unique values.
180+
*/
181+
void triggerCompleterUpdate();
182+
183+
/** Updates the values shown in the completer list.
184+
* @param values list of string values to show
185+
*/
186+
void updateCompleter( const QStringList& values );
187+
188+
/** Called when the gatherer thread is complete, regardless of whether it finished collecting values.
189+
* Cleans up the gatherer thread and triggers a new background thread if the widget's text has changed
190+
* in the meantime.
191+
*/
192+
void gathererThreadFinished();
193+
194+
private:
195+
196+
QgsVectorLayer* mLayer;
197+
int mAttributeIndex;
198+
199+
//! Will be true when a background update of the completer values is occurring
200+
bool mUpdateRequested;
201+
202+
//! Timer to prevent multiple updates of autocomplete list
203+
QTimer mShowPopupTimer;
204+
205+
//! Background value gatherer thread
206+
QgsFieldValuesLineEditValuesGatherer* mGatherer;
207+
208+
//! Will be set to the latest completion text string which should be requested
209+
QString mRequestedCompletionText;
210+
211+
//! Kicks off the gathering of completer text values for a specified substring
212+
void updateCompletionList( const QString& substring );
213+
214+
};
215+
216+
217+
#endif //QGSFIELDVALUESLINEEDIT_H

5 commit comments

Comments
 (5)

m-kuhn commented on Dec 22, 2016

@m-kuhn
Member

Nice!!

I wonder if it could be made to support also expressions. For the RelationReference widget, this would be very handy to look for a feature preview text (expression) that contains some string.

Let's say you reference a person and show CONCAT( "name", ' ,', "street", ' ,', "city" ) and it would just match on any part of the expression (possibly compiled and serverside). I guess that would need to be implemented as another Gatherer?

nirvn commented on Dec 23, 2016

@nirvn
Contributor

@nyalldawson fantastic job.

What about making the search by form filter action the one shown by default in the layer toolbar? Right now, it's the search by expression, which isn't as user friendly as the form filter.

nyalldawson commented on Dec 23, 2016

@nyalldawson
CollaboratorAuthor

@nirvn sounds sensible - I'll +1 a PR if you open it. I'm on email only at the moment so can't make one myself.

nyalldawson commented on Dec 23, 2016

@nyalldawson
CollaboratorAuthor

@m-kuhn yes a new gatherer would be the correct approach. I'm wondering what the use case is here though? Are you just wanting to take advantage of the background loading for a combo box?

m-kuhn commented on Dec 23, 2016

@m-kuhn
Member
Please sign in to comment.