Skip to content
Permalink
Browse files

[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.
  • Loading branch information
nyalldawson committed Dec 22, 2016
1 parent e27822b commit 0bfc9bb6e3ac45c544f79642cb668a28c1f06d8c
@@ -75,6 +75,7 @@
%Include qgsfieldmodel.sip
%Include qgsfieldproxymodel.sip
%Include qgsfieldvalidator.sip
%Include qgsfieldvalueslineedit.sip
%Include qgsfiledropedit.sip
%Include qgsfilewidget.sip
%Include qgsfiledownloader.sip
@@ -0,0 +1,59 @@
/** \class QgsFieldValuesLineEdit
* \ingroup gui
* A line edit with an autocompleter which takes unique values from a vector layer's fields.
* The autocompleter is populated from the vector layer in the background to ensure responsive
* interaction with the widget.
* \note added in QGIS 3.0
*/
class QgsFieldValuesLineEdit: QgsFilterLineEdit
{
%TypeHeaderCode
#include <qgsfieldvalueslineedit.h>
%End
public:

/** Constructor for QgsFieldValuesLineEdit
* @param parent parent widget
*/
QgsFieldValuesLineEdit( QWidget *parent /TransferThis/ = nullptr );

virtual ~QgsFieldValuesLineEdit();

/** Sets the layer containing the field that values will be shown from.
* @param layer vector layer
* @see layer()
* @see setAttributeIndex()
*/
void setLayer( QgsVectorLayer* layer );

/** Returns the layer containing the field that values will be shown from.
* @see setLayer()
* @see attributeIndex()
*/
QgsVectorLayer* layer() const;

/** Sets the attribute index for the field containing values to show in the widget.
* @param index index of attribute
* @see attributeIndex()
* @see setLayer()
*/
void setAttributeIndex( int index );

/** Returns the attribute index for the field containing values shown in the widget.
* @see setAttributeIndex()
* @see layer()
*/
int attributeIndex() const;

signals:

/** Emitted when the layer associated with the widget changes.
* @param layer vector layer
*/
void layerChanged( QgsVectorLayer* layer );

/** Emitted when the field associated with the widget changes.
* @param index new attribute index for field
*/
void attributeIndexChanged( int index );
};
@@ -223,6 +223,7 @@ SET(QGIS_GUI_SRCS
qgsfieldmodel.cpp
qgsfieldproxymodel.cpp
qgsfieldvalidator.cpp
qgsfieldvalueslineedit.cpp
qgsfiledropedit.cpp
qgsfilewidget.cpp
qgsfilterlineedit.cpp
@@ -391,6 +392,7 @@ SET(QGIS_GUI_MOC_HDRS
qgsfieldmodel.h
qgsfieldproxymodel.h
qgsfieldvalidator.h
qgsfieldvalueslineedit.h
qgsfiledropedit.h
qgsfilewidget.h
qgsfilterlineedit.h
@@ -18,6 +18,7 @@
#include "qgsfields.h"
#include "qgsfieldvalidator.h"
#include "qgsexpression.h"
#include "qgsfieldvalueslineedit.h"
#include <QSettings>
#include <QHBoxLayout>

@@ -255,10 +256,20 @@ void QgsDefaultSearchWidgetWrapper::initWidget( QWidget* widget )
mContainer->setLayout( new QHBoxLayout() );
mContainer->layout()->setMargin( 0 );
mContainer->layout()->setContentsMargins( 0, 0, 0, 0 );
mLineEdit = new QgsFilterLineEdit();
QVariant::Type fldType = layer()->fields().at( mFieldIdx ).type();

if ( fldType == QVariant::String )
{
mLineEdit = new QgsFieldValuesLineEdit();
static_cast< QgsFieldValuesLineEdit* >( mLineEdit )->setLayer( layer() );
static_cast< QgsFieldValuesLineEdit* >( mLineEdit )->setAttributeIndex( mFieldIdx );
}
else
{
mLineEdit = new QgsFilterLineEdit();
}
mContainer->layout()->addWidget( mLineEdit );

QVariant::Type fldType = layer()->fields().at( mFieldIdx ).type();
if ( fldType == QVariant::String )
{
mCheckbox = new QCheckBox( QStringLiteral( "Case sensitive" ) );
@@ -17,7 +17,7 @@
#define QGSDEFAULTSEARCHWIDGETWRAPPER_H

#include "qgssearchwidgetwrapper.h"
#include <qgsfilterlineedit.h>
#include "qgsfilterlineedit.h"

#include <QCheckBox>

@@ -0,0 +1,137 @@
/***************************************************************************
qgsfieldvalueslineedit.cpp
-------------------------
Date : 20-08-2016
Copyright : (C) 2016 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#include "qgsfieldvalueslineedit.h"
#include "qgsvectorlayer.h"
#include "qgsfloatingwidget.h"
#include <QCompleter>
#include <QStringListModel>
#include <QTimer>
#include <QHBoxLayout>

QgsFieldValuesLineEdit::QgsFieldValuesLineEdit( QWidget *parent )
: QgsFilterLineEdit( parent )
, mLayer( nullptr )
, mAttributeIndex( -1 )
, mUpdateRequested( false )
, mGatherer( nullptr )
{
QCompleter* c = new QCompleter( this );
c->setCaseSensitivity( Qt::CaseInsensitive );
c->setFilterMode( Qt::MatchContains );
setCompleter( c );
connect( this, &QgsFieldValuesLineEdit::textEdited, this, &QgsFieldValuesLineEdit::requestCompleterUpdate );
mShowPopupTimer.setSingleShot( true );
mShowPopupTimer.setInterval( 100 );
connect( &mShowPopupTimer, &QTimer::timeout, this, &QgsFieldValuesLineEdit::triggerCompleterUpdate );
}

QgsFieldValuesLineEdit::~QgsFieldValuesLineEdit()
{
if ( mGatherer )
{
mGatherer->stop();
mGatherer->wait(); // mGatherer is deleted when wait completes
}
}

void QgsFieldValuesLineEdit::setLayer( QgsVectorLayer* layer )
{
if ( mLayer == layer )
return;

mLayer = layer;
emit layerChanged( layer );
}

void QgsFieldValuesLineEdit::setAttributeIndex( int index )
{
if ( mAttributeIndex == index )
return;

mAttributeIndex = index;
emit attributeIndexChanged( index );
}

void QgsFieldValuesLineEdit::requestCompleterUpdate()
{
mUpdateRequested = true;
mShowPopupTimer.start();
}

void QgsFieldValuesLineEdit::triggerCompleterUpdate()
{
mShowPopupTimer.stop();
QString currentText = text();

if ( currentText.isEmpty() )
{
if ( mGatherer )
mGatherer->stop();
return;
}

updateCompletionList( currentText );
}

void QgsFieldValuesLineEdit::updateCompletionList( const QString &text )
{
if ( text.isEmpty() )
{
if ( mGatherer )
mGatherer->stop();
return;
}

mUpdateRequested = true;
if ( mGatherer )
{
mRequestedCompletionText = text;
mGatherer->stop();
return;
}

mGatherer = new QgsFieldValuesLineEditValuesGatherer( mLayer, mAttributeIndex );
mGatherer->setSubstring( text );

connect( mGatherer, &QgsFieldValuesLineEditValuesGatherer::collectedValues, this, &QgsFieldValuesLineEdit::updateCompleter );
connect( mGatherer, &QgsFieldValuesLineEditValuesGatherer::finished, this, &QgsFieldValuesLineEdit::gathererThreadFinished );

mGatherer->start();
}

void QgsFieldValuesLineEdit::gathererThreadFinished()
{
bool wasCancelled = mGatherer->wasCancelled();

delete mGatherer;
mGatherer = nullptr;

if ( wasCancelled )
{
QString text = mRequestedCompletionText;
mRequestedCompletionText.clear();
updateCompletionList( text );
return;
}
}

void QgsFieldValuesLineEdit::updateCompleter( const QStringList& values )
{
mUpdateRequested = false;
completer()->setModel( new QStringListModel( values ) );
completer()->complete();
}

5 comments on commit 0bfc9bb

@m-kuhn

This comment has been minimized.

Copy link
Member

@m-kuhn m-kuhn replied Dec 22, 2016

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

This comment has been minimized.

Copy link
Contributor

@nirvn nirvn replied Dec 23, 2016

@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

This comment has been minimized.

Copy link
Collaborator Author

@nyalldawson nyalldawson replied Dec 23, 2016

@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

This comment has been minimized.

Copy link
Collaborator Author

@nyalldawson nyalldawson replied Dec 23, 2016

@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

This comment has been minimized.

Copy link
Member

@m-kuhn m-kuhn replied Dec 23, 2016

Please sign in to comment.
You can’t perform that action at this time.