Skip to content

Commit

Permalink
[FEATURE] enable auto completion in locator
Browse files Browse the repository at this point in the history
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
  • Loading branch information
3nids committed Sep 8, 2020
1 parent 014de96 commit 002a703
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 13 deletions.
23 changes: 23 additions & 0 deletions python/core/auto_generated/locator/qgslocator.sip.in
Expand Up @@ -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:
Expand All @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion python/core/auto_generated/locator/qgslocatorfilter.sip.in
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions python/gui/auto_generated/locator/qgslocatorwidget.sip.in
Expand Up @@ -10,6 +10,7 @@




class QgsLocatorWidget : QWidget
{
%Docstring
Expand Down
16 changes: 14 additions & 2 deletions src/core/locator/qgslocator.cpp
Expand Up @@ -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
Expand Down Expand Up @@ -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 )
{
Expand All @@ -191,7 +202,6 @@ void QgsLocator::fetchResults( const QString &string, const QgsLocatorContext &c
}
else
{
// run filter in background
threadedFilters.append( clone.release() );
}
}
Expand Down Expand Up @@ -220,6 +230,8 @@ void QgsLocator::fetchResults( const QString &string, const QgsLocatorContext &c
thread->start();
}

emit searchPrepared();

if ( mActiveThreads.empty() )
emit finished();
}
Expand Down
22 changes: 22 additions & 0 deletions src/core/locator/qgslocator.h
Expand Up @@ -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:

/**
Expand All @@ -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.
Expand All @@ -171,6 +191,8 @@ class CORE_EXPORT QgsLocator : public QObject
QList< QgsLocatorFilter * > mFilters;
QList< QThread * > mActiveThreads;

QStringList mAutocompleList;

void cancelRunningQuery();

};
Expand Down
3 changes: 2 additions & 1 deletion src/core/locator/qgslocatorfilter.h
Expand Up @@ -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
Expand Down
87 changes: 80 additions & 7 deletions src/gui/locator/qgslocatorwidget.cpp
Expand Up @@ -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 );
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -330,7 +336,6 @@ void QgsLocatorWidget::configMenuAboutToShow()
}



void QgsLocatorWidget::acceptCurrentEntry()
{
if ( mModelBridge->hasQueueRequested() )
Expand All @@ -352,8 +357,6 @@ void QgsLocatorWidget::acceptCurrentEntry()
}
}



///@cond PRIVATE

//
Expand Down Expand Up @@ -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
31 changes: 29 additions & 2 deletions src/gui/locator/qgslocatorwidget.h
Expand Up @@ -21,17 +21,19 @@
#include "qgis_gui.h"
#include "qgslocatorfilter.h"
#include "qgsfloatingwidget.h"
#include "qgsfilterlineedit.h"

#include <QWidget>
#include <QTreeView>
#include <QFocusEvent>
#include <QHeaderView>
#include <QTimer>

class QgsLocator;
class QgsFilterLineEdit;
class QgsLocatorResultsView;
class QgsMapCanvas;
class QgsLocatorModelBridge;
class QgsLocatorLineEdit;

/**
* \class QgsLocatorWidget
Expand Down Expand Up @@ -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;
Expand All @@ -110,6 +112,7 @@ class GUI_EXPORT QgsLocatorWidget : public QWidget
bool mHasSelectedResult = false;

void acceptCurrentEntry();

};

#ifndef SIP_RUN
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 002a703

Please sign in to comment.