From 002a7033f09f714ef2671d0e778e5418ab513539 Mon Sep 17 00:00:00 2001 From: Denis Rouzaud Date: Mon, 7 Sep 2020 15:23:56 +0200 Subject: [PATCH] [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 --- .../auto_generated/locator/qgslocator.sip.in | 23 +++++ .../locator/qgslocatorfilter.sip.in | 3 +- .../locator/qgslocatorwidget.sip.in | 1 + src/core/locator/qgslocator.cpp | 16 +++- src/core/locator/qgslocator.h | 22 +++++ src/core/locator/qgslocatorfilter.h | 3 +- src/gui/locator/qgslocatorwidget.cpp | 87 +++++++++++++++++-- src/gui/locator/qgslocatorwidget.h | 31 ++++++- 8 files changed, 173 insertions(+), 13 deletions(-) diff --git a/python/core/auto_generated/locator/qgslocator.sip.in b/python/core/auto_generated/locator/qgslocator.sip.in index c873d88042f..b7020c22f5b 100644 --- a/python/core/auto_generated/locator/qgslocator.sip.in +++ b/python/core/auto_generated/locator/qgslocator.sip.in @@ -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(); diff --git a/python/core/auto_generated/locator/qgslocatorfilter.sip.in b/python/core/auto_generated/locator/qgslocatorfilter.sip.in index 29d175e5a60..564d35e0d91 100644 --- a/python/core/auto_generated/locator/qgslocatorfilter.sip.in +++ b/python/core/auto_generated/locator/qgslocatorfilter.sip.in @@ -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; diff --git a/python/gui/auto_generated/locator/qgslocatorwidget.sip.in b/python/gui/auto_generated/locator/qgslocatorwidget.sip.in index 5138c4dab9e..8824bfc0917 100644 --- a/python/gui/auto_generated/locator/qgslocatorwidget.sip.in +++ b/python/gui/auto_generated/locator/qgslocatorwidget.sip.in @@ -10,6 +10,7 @@ + class QgsLocatorWidget : QWidget { %Docstring diff --git a/src/core/locator/qgslocator.cpp b/src/core/locator/qgslocator.cpp index 19886dbec15..0e9082c18ea 100644 --- a/src/core/locator/qgslocator.cpp +++ b/src/core/locator/qgslocator.cpp @@ -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(); } diff --git a/src/core/locator/qgslocator.h b/src/core/locator/qgslocator.h index 8bd45a93246..f30d7f13c73 100644 --- a/src/core/locator/qgslocator.h +++ b/src/core/locator/qgslocator.h @@ -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(); }; diff --git a/src/core/locator/qgslocatorfilter.h b/src/core/locator/qgslocatorfilter.h index 3f26de89f98..b921d372ad8 100644 --- a/src/core/locator/qgslocatorfilter.h +++ b/src/core/locator/qgslocatorfilter.h @@ -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 diff --git a/src/gui/locator/qgslocatorwidget.cpp b/src/gui/locator/qgslocatorwidget.cpp index 21c93a8bda7..afa14849260 100644 --- a/src/gui/locator/qgslocatorwidget.cpp +++ b/src/gui/locator/qgslocatorwidget.cpp @@ -24,14 +24,17 @@ #include "qgsapplication.h" #include "qgslogger.h" #include "qgsguiutils.h" + #include #include #include +#include +#include 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 diff --git a/src/gui/locator/qgslocatorwidget.h b/src/gui/locator/qgslocatorwidget.h index b90eaa69174..4659300a682 100644 --- a/src/gui/locator/qgslocatorwidget.h +++ b/src/gui/locator/qgslocatorwidget.h @@ -21,6 +21,8 @@ #include "qgis_gui.h" #include "qgslocatorfilter.h" #include "qgsfloatingwidget.h" +#include "qgsfilterlineedit.h" + #include #include #include @@ -28,10 +30,10 @@ #include 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