[FEATURE] enable auto completion in locator

a locator filter can now return a completion list while preparing the search
the line edit will use the first matching completion and display it as light grey text
the completion can be triggered by pressing Tab key
This commit is contained in:
Denis Rouzaud 2020-09-07 15:23:56 +02:00
parent 014de9614d
commit 002a7033f0
8 changed files with 173 additions and 13 deletions

View File

@ -131,6 +131,14 @@ Returns ``True`` if a query is currently being executed by the locator.
Will call clearPreviousResults on all filters
.. versionadded:: 3.2
%End
QStringList completionList() const;
%Docstring
Returns the list for auto completion
This list is updated when preparing the search
.. versionadded:: 3.16
%End
signals:
@ -139,6 +147,21 @@ Will call clearPreviousResults on all filters
%Docstring
Emitted whenever a filter encounters a matching ``result`` after the :py:func:`~QgsLocator.fetchResults` method
is called.
%End
void searchBegan();
%Docstring
Emitted when locator has begun a search, before actualy preparing it.
.. versionadded:: 3.16
%End
void searchPrepared();
%Docstring
Emitted when locator has prepared the search (:py:func:`QgsLocatorFilter.prepare`)
before the search is actually performed
.. versionadded:: 3.16
%End
void finished();

View File

@ -163,12 +163,13 @@ results from this filter.
.. seealso:: :py:func:`activePrefix`
%End
virtual void prepare( const QString &string, const QgsLocatorContext &context );
virtual QStringList prepare( const QString &string, const QgsLocatorContext &context );
%Docstring
Prepares the filter instance for an upcoming search for the specified ``string``. This method is always called
from the main thread, and individual filter subclasses should perform whatever
tasks are required in order to allow a subsequent search to safely execute
on a background thread.
The method return an autocompletion list
%End
virtual void fetchResults( const QString &string, const QgsLocatorContext &context, QgsFeedback *feedback ) = 0;

View File

@ -10,6 +10,7 @@
class QgsLocatorWidget : QWidget
{
%Docstring

View File

@ -124,6 +124,9 @@ void QgsLocator::registerFilter( QgsLocatorFilter *filter )
void QgsLocator::fetchResults( const QString &string, const QgsLocatorContext &c, QgsFeedback *feedback )
{
mAutocompleList.clear();
emit searchBegan();
QgsLocatorContext context( c );
// ideally this should not be required, as well behaved callers
// will NOT fire up a new fetchResults call while an existing one is
@ -182,7 +185,15 @@ void QgsLocator::fetchResults( const QString &string, const QgsLocatorContext &c
result.filter = filter;
filterSentResult( result );
} );
clone->prepare( searchString, context );
QStringList autoCompleteList = clone->prepare( searchString, context );
if ( context.usingPrefix )
{
for ( int i = 0; i < autoCompleteList.length(); i++ )
{
autoCompleteList[i].prepend( QStringLiteral( "%1 " ).arg( prefix ) );
}
}
mAutocompleList.append( autoCompleteList );
if ( clone->flags() & QgsLocatorFilter::FlagFast )
{
@ -191,7 +202,6 @@ void QgsLocator::fetchResults( const QString &string, const QgsLocatorContext &c
}
else
{
// run filter in background
threadedFilters.append( clone.release() );
}
}
@ -220,6 +230,8 @@ void QgsLocator::fetchResults( const QString &string, const QgsLocatorContext &c
thread->start();
}
emit searchPrepared();
if ( mActiveThreads.empty() )
emit finished();
}

View File

@ -145,6 +145,13 @@ class CORE_EXPORT QgsLocator : public QObject
*/
void clearPreviousResults();
/**
* Returns the list for auto completion
* This list is updated when preparing the search
* \since QGIS 3.16
*/
QStringList completionList() const {return mAutocompleList;}
signals:
/**
@ -153,6 +160,19 @@ class CORE_EXPORT QgsLocator : public QObject
*/
void foundResult( const QgsLocatorResult &result );
/**
* Emitted when locator has begun a search, before actualy preparing it.
* \since QGIS 3.16
*/
void searchBegan();
/**
* Emitted when locator has prepared the search (\see QgsLocatorFilter::prepare)
* before the search is actually performed
* \since QGIS 3.16
*/
void searchPrepared();
/**
* Emitted when locator has finished a query, either as a result
* of successful completion or early cancellation.
@ -171,6 +191,8 @@ class CORE_EXPORT QgsLocator : public QObject
QList< QgsLocatorFilter * > mFilters;
QList< QThread * > mActiveThreads;
QStringList mAutocompleList;
void cancelRunningQuery();
};

View File

@ -219,8 +219,9 @@ class CORE_EXPORT QgsLocatorFilter : public QObject
* from the main thread, and individual filter subclasses should perform whatever
* tasks are required in order to allow a subsequent search to safely execute
* on a background thread.
* The method return an autocompletion list
*/
virtual void prepare( const QString &string, const QgsLocatorContext &context ) { Q_UNUSED( string ) Q_UNUSED( context ); }
virtual QStringList prepare( const QString &string, const QgsLocatorContext &context ) { Q_UNUSED( string ) Q_UNUSED( context ); return QStringList();}
/**
* Retrieves the filter results for a specified search \a string. The \a context

View File

@ -24,14 +24,17 @@
#include "qgsapplication.h"
#include "qgslogger.h"
#include "qgsguiutils.h"
#include <QLayout>
#include <QCompleter>
#include <QMenu>
#include <QTextLayout>
#include <QTextLine>
QgsLocatorWidget::QgsLocatorWidget( QWidget *parent )
: QWidget( parent )
, mModelBridge( new QgsLocatorModelBridge( this ) )
, mLineEdit( new QgsFilterLineEdit() )
, mLineEdit( new QgsLocatorLineEdit( this ) )
, mResultsView( new QgsLocatorResultsView() )
{
mLineEdit->setShowClearButton( true );
@ -85,7 +88,7 @@ QgsLocatorWidget::QgsLocatorWidget( QWidget *parent )
connect( mModelBridge, &QgsLocatorModelBridge::resultAdded, this, &QgsLocatorWidget::resultAdded );
connect( mModelBridge, &QgsLocatorModelBridge::isRunningChanged, this, [ = ]() {mLineEdit->setShowSpinner( mModelBridge->isRunning() );} );
connect( mModelBridge, & QgsLocatorModelBridge::resultsCleared, this, [ = ]() {mHasSelectedResult = false;} );
connect( mModelBridge, &QgsLocatorModelBridge::resultsCleared, this, [ = ]() {mHasSelectedResult = false;} );
// have a tiny delay between typing text in line edit and showing the window
mPopupTimer.setInterval( 100 );
@ -260,8 +263,11 @@ bool QgsLocatorWidget::eventFilter( QObject *obj, QEvent *event )
mResultsContainer->hide();
return true;
case Qt::Key_Tab:
mHasSelectedResult = true;
mResultsView->selectNextResult();
if ( !mLineEdit->performCompletion() )
{
mHasSelectedResult = true;
mResultsView->selectNextResult();
}
return true;
case Qt::Key_Backtab:
mHasSelectedResult = true;
@ -330,7 +336,6 @@ void QgsLocatorWidget::configMenuAboutToShow()
}
void QgsLocatorWidget::acceptCurrentEntry()
{
if ( mModelBridge->hasQueueRequested() )
@ -352,8 +357,6 @@ void QgsLocatorWidget::acceptCurrentEntry()
}
}
///@cond PRIVATE
//
@ -449,5 +452,75 @@ void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result )
mLocator->search( result.userData.toString() );
}
QgsLocatorLineEdit::QgsLocatorLineEdit( QgsLocatorWidget *locator, QWidget *parent )
: QgsFilterLineEdit( parent )
, mLocatorWidget( locator )
{
connect( mLocatorWidget->locator(), &QgsLocator::searchPrepared, this, [&] { update(); } );
}
void QgsLocatorLineEdit::paintEvent( QPaintEvent *event )
{
// this adds the completion as grey text at the right of the cursor
// see https://stackoverflow.com/a/50425331/1548052
// this is possible that the completion might be badly rendered if the cursor is larger than the line edit
// this sounds acceptable as it is not very likely to use completion for super long texts
// for more details see https://stackoverflow.com/a/54218192/1548052
QLineEdit::paintEvent( event );
if ( !hasFocus() )
return;
QString currentText = text();
if ( currentText.length() == 0 || cursorPosition() < currentText.length() )
return;
const QStringList completionList = mLocatorWidget->locator()->completionList();
QString completion;
for ( const QString &candidate : completionList )
{
if ( candidate.startsWith( currentText ) )
{
completion = candidate.right( candidate.length() - currentText.length() );
mCompletionText = candidate;
break;
}
}
if ( completion.isEmpty() )
return;
ensurePolished(); // ensure font() is up to date
QRect cr = cursorRect();
QPoint pos = cr.topRight() - QPoint( cr.width() / 2, 0 );
QTextLayout l( completion, font() );
l.beginLayout();
QTextLine line = l.createLine();
line.setLineWidth( width() - pos.x() );
line.setPosition( pos );
l.endLayout();
QPainter p( this );
p.setPen( QPen( Qt::gray, 1 ) );
l.draw( &p, QPoint( 0, 0 ) );
}
bool QgsLocatorLineEdit::performCompletion()
{
if ( !mCompletionText.isEmpty() )
{
setText( mCompletionText );
mCompletionText.clear();
return true;
}
else
return false;
}
///@endcond

View File

@ -21,6 +21,8 @@
#include "qgis_gui.h"
#include "qgslocatorfilter.h"
#include "qgsfloatingwidget.h"
#include "qgsfilterlineedit.h"
#include <QWidget>
#include <QTreeView>
#include <QFocusEvent>
@ -28,10 +30,10 @@
#include <QTimer>
class QgsLocator;
class QgsFilterLineEdit;
class QgsLocatorResultsView;
class QgsMapCanvas;
class QgsLocatorModelBridge;
class QgsLocatorLineEdit;
/**
* \class QgsLocatorWidget
@ -98,7 +100,7 @@ class GUI_EXPORT QgsLocatorWidget : public QWidget
private:
QgsLocatorModelBridge *mModelBridge = nullptr;
QgsFilterLineEdit *mLineEdit = nullptr;
QgsLocatorLineEdit *mLineEdit = nullptr;
QgsFloatingWidget *mResultsContainer = nullptr;
QgsLocatorResultsView *mResultsView = nullptr;
QgsMapCanvas *mMapCanvas = nullptr;
@ -110,6 +112,7 @@ class GUI_EXPORT QgsLocatorWidget : public QWidget
bool mHasSelectedResult = false;
void acceptCurrentEntry();
};
#ifndef SIP_RUN
@ -171,6 +174,30 @@ class GUI_EXPORT QgsLocatorResultsView : public QTreeView
};
/**
* \class QgsLocatorLineEdit
* \ingroup gui
* Custom line edit to handle completion within the line edit as a light gray text
* \since QGIS 3.16
*/
class QgsLocatorLineEdit : public QgsFilterLineEdit
{
Q_OBJECT
public:
explicit QgsLocatorLineEdit( QgsLocatorWidget *locator, QWidget *parent = nullptr );
//! Perform completion and returns true if successful
bool performCompletion();
protected:
void paintEvent( QPaintEvent *event ) override;
private:
QgsLocatorWidget *mLocatorWidget = nullptr;
QString mCompletionText = nullptr;
};
///@endcond
#endif