Skip to content

Commit

Permalink
[FEATURE] Show field values in autocompleter in form filter mode
Browse files Browse the repository at this point in the history
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 0bfc9bb
Show file tree
Hide file tree
Showing 7 changed files with 430 additions and 3 deletions.
1 change: 1 addition & 0 deletions python/gui/gui.sip
Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions python/gui/qgsfieldvalueslineedit.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 );
};
2 changes: 2 additions & 0 deletions src/gui/CMakeLists.txt
Expand Up @@ -223,6 +223,7 @@ SET(QGIS_GUI_SRCS
qgsfieldmodel.cpp
qgsfieldproxymodel.cpp
qgsfieldvalidator.cpp
qgsfieldvalueslineedit.cpp
qgsfiledropedit.cpp
qgsfilewidget.cpp
qgsfilterlineedit.cpp
Expand Down Expand Up @@ -391,6 +392,7 @@ SET(QGIS_GUI_MOC_HDRS
qgsfieldmodel.h
qgsfieldproxymodel.h
qgsfieldvalidator.h
qgsfieldvalueslineedit.h
qgsfiledropedit.h
qgsfilewidget.h
qgsfilterlineedit.h
Expand Down
15 changes: 13 additions & 2 deletions src/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.cpp
Expand Up @@ -18,6 +18,7 @@
#include "qgsfields.h"
#include "qgsfieldvalidator.h"
#include "qgsexpression.h"
#include "qgsfieldvalueslineedit.h"
#include <QSettings>
#include <QHBoxLayout>

Expand Down Expand Up @@ -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" ) );
Expand Down
2 changes: 1 addition & 1 deletion src/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.h
Expand Up @@ -17,7 +17,7 @@
#define QGSDEFAULTSEARCHWIDGETWRAPPER_H

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

#include <QCheckBox>

Expand Down
137 changes: 137 additions & 0 deletions src/gui/qgsfieldvalueslineedit.cpp
@@ -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
Copy link
Member

@m-kuhn m-kuhn commented on 0bfc9bb Dec 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

@nirvn nirvn commented on 0bfc9bb Dec 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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
Copy link
Member

@m-kuhn m-kuhn commented on 0bfc9bb Dec 23, 2016 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.