diff --git a/python/core/auto_generated/locator/qgslocatorfilter.sip.in b/python/core/auto_generated/locator/qgslocatorfilter.sip.in index d7be0553620..13efa78de0d 100644 --- a/python/core/auto_generated/locator/qgslocatorfilter.sip.in +++ b/python/core/auto_generated/locator/qgslocatorfilter.sip.in @@ -10,6 +10,7 @@ + class QgsLocatorResult { %Docstring @@ -47,8 +48,31 @@ Constructor for QgsLocatorResult. QString group; + struct ResultAction + { + public: + ResultAction(); +%Docstring +Constructor for ResultAction +%End + + ResultAction( int id, QString text, QString iconPath = QString() ); +%Docstring +Constructor for ResultAction +The ``id`` used to recognized the action when the result is triggered. +It should be 0 or greater as otherwise, the result will be triggered +normally. +%End + int id; + QString text; + QString iconPath; + }; + + QList actions; }; + + class QgsLocatorFilter : QObject { %Docstring @@ -172,6 +196,16 @@ by a user. The filter subclass must implement logic here to perform the desired operation for the search result. E.g. a file search filter would open file associated with the triggered result. +%End + + virtual void triggerResultFromAction( const QgsLocatorResult &result, const int actionId ); +%Docstring +Triggers a filter ``result`` from this filter for an entry in the context menu. +The entry is identified by its ``actionId`` as specified in the result of this filter. + +.. seealso:: :py:func:`triggerResult` + +.. versionadded:: 3.6 %End virtual void clearPreviousResults(); diff --git a/python/core/auto_generated/locator/qgslocatormodel.sip.in b/python/core/auto_generated/locator/qgslocatormodel.sip.in index 56b0998155a..af2787813cf 100644 --- a/python/core/auto_generated/locator/qgslocatormodel.sip.in +++ b/python/core/auto_generated/locator/qgslocatormodel.sip.in @@ -36,6 +36,7 @@ in order to ensure correct sorting of results by priority and match level. ResultScoreRole, ResultFilterNameRole, ResultFilterGroupSortingRole, + ResultActionsRole, }; QgsLocatorModel( QObject *parent /TransferThis/ = 0 ); diff --git a/python/core/auto_generated/locator/qgslocatormodelbridge.sip.in b/python/core/auto_generated/locator/qgslocatormodelbridge.sip.in index aae26e9c275..21be58718e2 100644 --- a/python/core/auto_generated/locator/qgslocatormodelbridge.sip.in +++ b/python/core/auto_generated/locator/qgslocatormodelbridge.sip.in @@ -12,6 +12,7 @@ + class QgsLocatorModelBridge : QObject { %Docstring @@ -56,9 +57,9 @@ Returns true if some text to be search is pending in the queue Returns true if the a search is currently running %End - void triggerResult( const QModelIndex &index ); + void triggerResult( const QModelIndex &index, const int actionId = -1 ); %Docstring -Triggers the result at given index +Triggers the result at given ``index`` and with optional ``actionId`` if an additional action was triggered %End signals: diff --git a/scripts/sipify.pl b/scripts/sipify.pl index ad54fe6a5a8..31f5b8fe8d8 100755 --- a/scripts/sipify.pl +++ b/scripts/sipify.pl @@ -1006,7 +1006,8 @@ while ($LINE_IDX < $LINE_COUNT){ }; # remove struct member assignment - if ( $SIP_RUN != 1 && $ACCESS[$#ACCESS] == PUBLIC && $LINE =~ m/^(\s*\w+[\w<> *&:,]* \*?\w+) = [\-\w\:\.]+(\([^()]*\))?\s*;/ ){ + # https://regex101.com/r/tWRGkY/2 + if ( $SIP_RUN != 1 && $ACCESS[$#ACCESS] == PUBLIC && $LINE =~ m/^(\s*\w+[\w<> *&:,]* \*?\w+) = [\-\w\:\.]+(<\w+( \*)?>)?(\([^()]*\))?\s*;/ ){ dbg_info("remove struct member assignment"); $LINE = "$1;"; } diff --git a/src/app/locator/qgsinbuiltlocatorfilters.cpp b/src/app/locator/qgsinbuiltlocatorfilters.cpp index d39db377e4a..104107face6 100644 --- a/src/app/locator/qgsinbuiltlocatorfilters.cpp +++ b/src/app/locator/qgsinbuiltlocatorfilters.cpp @@ -15,6 +15,8 @@ * * ***************************************************************************/ +#include +#include #include "qgsinbuiltlocatorfilters.h" #include "qgsproject.h" @@ -25,8 +27,7 @@ #include "qgsmaplayermodel.h" #include "qgslayoutmanager.h" #include "qgsmapcanvas.h" -#include -#include +#include "qgsfeatureaction.h" QgsLayerTreeLocatorFilter::QgsLayerTreeLocatorFilter( QObject *parent ) : QgsLocatorFilter( parent ) @@ -400,6 +401,8 @@ void QgsAllLayersFeaturesLocatorFilter::fetchResults( const QString &string, con result.userData = QVariantList() << f.id() << preparedLayer.layerId; result.icon = preparedLayer.layerIcon; result.score = static_cast< double >( string.length() ) / result.displayString.size(); + + result.actions << QgsLocatorResult::ResultAction( OpenForm, tr( "Open form…" ) ); emit resultFetched( result ); foundInCurrentLayer++; @@ -413,15 +416,41 @@ void QgsAllLayersFeaturesLocatorFilter::fetchResults( const QString &string, con } void QgsAllLayersFeaturesLocatorFilter::triggerResult( const QgsLocatorResult &result ) +{ + triggerResultFromAction( result, NoEntry ); +} + +void QgsAllLayersFeaturesLocatorFilter::triggerResultFromAction( const QgsLocatorResult &result, const int actionId ) { QVariantList dataList = result.userData.toList(); - QgsFeatureId id = dataList.at( 0 ).toLongLong(); + QgsFeatureId fid = dataList.at( 0 ).toLongLong(); QString layerId = dataList.at( 1 ).toString(); QgsVectorLayer *layer = qobject_cast< QgsVectorLayer *>( QgsProject::instance()->mapLayer( layerId ) ); if ( !layer ) return; - QgisApp::instance()->mapCanvas()->zoomToFeatureIds( layer, QgsFeatureIds() << id ); + if ( actionId == OpenForm ) + { + QgsFeature f; + QgsFeatureRequest request; + request.setFilterFid( fid ); + bool fetched = layer->getFeatures( request ).nextFeature( f ); + if ( !fetched ) + return; + QgsFeatureAction action( tr( "Attributes changed" ), f, layer, QString(), -1, QgisApp::instance() ); + if ( layer->isEditable() ) + { + action.editFeature( false ); + } + else + { + action.viewFeatureForm(); + } + } + else + { + QgisApp::instance()->mapCanvas()->zoomToFeatureIds( layer, QgsFeatureIds() << fid ); + } } // diff --git a/src/app/locator/qgsinbuiltlocatorfilters.h b/src/app/locator/qgsinbuiltlocatorfilters.h index 9b28fa6bf25..f96ce150825 100644 --- a/src/app/locator/qgsinbuiltlocatorfilters.h +++ b/src/app/locator/qgsinbuiltlocatorfilters.h @@ -119,6 +119,12 @@ class APP_EXPORT QgsAllLayersFeaturesLocatorFilter : public QgsLocatorFilter Q_OBJECT public: + enum ContextMenuEntry + { + NoEntry, + OpenForm + }; + struct PreparedLayer { public: @@ -128,7 +134,7 @@ class APP_EXPORT QgsAllLayersFeaturesLocatorFilter : public QgsLocatorFilter QString layerName; QString layerId; QIcon layerIcon; - } ; + }; QgsAllLayersFeaturesLocatorFilter( QObject *parent = nullptr ); QgsAllLayersFeaturesLocatorFilter *clone() const override; @@ -140,6 +146,7 @@ class APP_EXPORT QgsAllLayersFeaturesLocatorFilter : public QgsLocatorFilter void prepare( const QString &string, const QgsLocatorContext &context ) override; void fetchResults( const QString &string, const QgsLocatorContext &context, QgsFeedback *feedback ) override; void triggerResult( const QgsLocatorResult &result ) override; + void triggerResultFromAction( const QgsLocatorResult &result, const int actionId ) override; private: int mMaxResultsPerLayer = 6; diff --git a/src/core/locator/qgslocatorfilter.cpp b/src/core/locator/qgslocatorfilter.cpp index a156369a751..6922b6a49c6 100644 --- a/src/core/locator/qgslocatorfilter.cpp +++ b/src/core/locator/qgslocatorfilter.cpp @@ -33,6 +33,12 @@ QgsLocatorFilter::Flags QgsLocatorFilter::flags() const return nullptr; } +void QgsLocatorFilter::triggerResultFromAction( const QgsLocatorResult &result, const int actionId ) +{ + Q_UNUSED( result ); + Q_UNUSED( actionId ); +} + bool QgsLocatorFilter::stringMatches( const QString &candidate, const QString &search ) { return !search.isEmpty() && candidate.contains( search, Qt::CaseInsensitive ); diff --git a/src/core/locator/qgslocatorfilter.h b/src/core/locator/qgslocatorfilter.h index c6fd9c9c3b2..a860a6d169e 100644 --- a/src/core/locator/qgslocatorfilter.h +++ b/src/core/locator/qgslocatorfilter.h @@ -18,12 +18,14 @@ #ifndef QGSLOCATORFILTER_H #define QGSLOCATORFILTER_H +#include +#include +#include +#include + #include "qgis_core.h" #include "qgslocatorcontext.h" #include "qgslogger.h" -#include -#include -#include class QgsFeedback; class QgsLocatorFilter; @@ -87,12 +89,50 @@ class CORE_EXPORT QgsLocatorResult * If left as empty string, this means that results are all shown without being grouped. * If a group is given, the results will be grouped by \a group under a header. * \note This should be translated. - * \since 3.2 + * \since QGIS 3.2 */ QString group = QString(); + /** + * The ResultAction stores basic information for additional + * actions to be used in a locator widget for the result. + * They could be used in a context menu for instance. + * \since QGIS 3.6 + */ + struct CORE_EXPORT ResultAction + { + public: + //! Constructor for ResultAction + ResultAction() = default; + + /** + * Constructor for ResultAction + * The \a id used to recognized the action when the result is triggered. + * It should be 0 or greater as otherwise, the result will be triggered + * normally. + */ + ResultAction( int id, QString text, QString iconPath = QString() ) + : id( id ) + , text( text ) + , iconPath( iconPath ) + {} + int id = -1; + QString text; + QString iconPath; + }; + + /** + * Additional actions to be used in a locator widget + * for the given result. They could be displayed in + * a context menu. + * \since QGIS 3.6 + */ + QList actions; }; +Q_DECLARE_METATYPE( QgsLocatorResult::ResultAction ) + + /** * \class QgsLocatorFilter * \ingroup core @@ -209,6 +249,14 @@ class CORE_EXPORT QgsLocatorFilter : public QObject */ virtual void triggerResult( const QgsLocatorResult &result ) = 0; + /** + * Triggers a filter \a result from this filter for an entry in the context menu. + * The entry is identified by its \a actionId as specified in the result of this filter. + * \see triggerResult() + * \since QGIS 3.6 + */ + virtual void triggerResultFromAction( const QgsLocatorResult &result, const int actionId ); + /** * This method will be called on main thread on the original filter (not a clone) * before fetching results or before triggering a result to clear any change made diff --git a/src/core/locator/qgslocatormodel.cpp b/src/core/locator/qgslocatormodel.cpp index 688d4554307..195e081442b 100644 --- a/src/core/locator/qgslocatormodel.cpp +++ b/src/core/locator/qgslocatormodel.cpp @@ -160,6 +160,9 @@ QVariant QgsLocatorModel::data( const QModelIndex &index, int role ) const return 1; else return 0; + + case ResultActionsRole: + return QVariant::fromValue( mResults.at( index.row() ).result.actions ); } return QVariant(); @@ -188,6 +191,7 @@ QHash QgsLocatorModel::roleNames() const roles[ResultScoreRole] = "ResultScore"; roles[ResultFilterNameRole] = "ResultFilterName"; roles[ResultFilterGroupSortingRole] = "ResultFilterGroupSorting"; + roles[ResultActionsRole] = "ResultContextMenuActions"; roles[Qt::DisplayRole] = "Text"; return roles; } diff --git a/src/core/locator/qgslocatormodel.h b/src/core/locator/qgslocatormodel.h index 8a31efdac3f..97e9eecfbce 100644 --- a/src/core/locator/qgslocatormodel.h +++ b/src/core/locator/qgslocatormodel.h @@ -56,6 +56,7 @@ class CORE_EXPORT QgsLocatorModel : public QAbstractTableModel ResultScoreRole, //!< Result match score, used by QgsLocatorProxyModel for sorting roles. ResultFilterNameRole, //!< Associated filter name which created the result ResultFilterGroupSortingRole, //!< Group results within the same filter results + ResultActionsRole, //!< The actions to be shown for the given result in a context menu }; /** diff --git a/src/core/locator/qgslocatormodelbridge.cpp b/src/core/locator/qgslocatormodelbridge.cpp index 943453821cb..51d7ae8553e 100644 --- a/src/core/locator/qgslocatormodelbridge.cpp +++ b/src/core/locator/qgslocatormodelbridge.cpp @@ -37,12 +37,17 @@ bool QgsLocatorModelBridge::isRunning() const return mIsRunning; } -void QgsLocatorModelBridge::triggerResult( const QModelIndex &index ) +void QgsLocatorModelBridge::triggerResult( const QModelIndex &index, const int actionId ) { mLocator->clearPreviousResults(); QgsLocatorResult result = mProxyModel->data( index, QgsLocatorModel::ResultDataRole ).value< QgsLocatorResult >(); if ( result.filter ) - result.filter->triggerResult( result ); + { + if ( actionId >= 0 ) + result.filter->triggerResultFromAction( result, actionId ); + else + result.filter->triggerResult( result ); + } } void QgsLocatorModelBridge::setIsRunning( bool isRunning ) diff --git a/src/core/locator/qgslocatormodelbridge.h b/src/core/locator/qgslocatormodelbridge.h index db88253102c..79af5414653 100644 --- a/src/core/locator/qgslocatormodelbridge.h +++ b/src/core/locator/qgslocatormodelbridge.h @@ -24,6 +24,8 @@ #include "qgscoordinatereferencesystem.h" #include "qgsrectangle.h" +class QAction; + class QgsLocatorResult; class QgsLocator; class QgsLocatorContext; @@ -61,8 +63,8 @@ class CORE_EXPORT QgsLocatorModelBridge : public QObject //! Returns true if the a search is currently running bool isRunning() const; - //! Triggers the result at given index - void triggerResult( const QModelIndex &index ); + //! Triggers the result at given \a index and with optional \a actionId if an additional action was triggered + void triggerResult( const QModelIndex &index, const int actionId = -1 ); signals: //! Emitted when a result is added diff --git a/src/gui/locator/qgslocatorwidget.cpp b/src/gui/locator/qgslocatorwidget.cpp index 6b4a6f7ce40..da78fdd0403 100644 --- a/src/gui/locator/qgslocatorwidget.cpp +++ b/src/gui/locator/qgslocatorwidget.cpp @@ -74,9 +74,12 @@ QgsLocatorWidget::QgsLocatorWidget( QWidget *parent ) mResultsView->setUniformRowHeights( true ); mResultsView->setIconSize( QSize( 16, 16 ) ); mResultsView->recalculateSize(); + mResultsView->setContextMenuPolicy( Qt::CustomContextMenu ); connect( mLineEdit, &QLineEdit::textChanged, this, &QgsLocatorWidget::scheduleDelayedPopup ); connect( mResultsView, &QAbstractItemView::activated, this, &QgsLocatorWidget::acceptCurrentEntry ); + connect( mResultsView, &QAbstractItemView::customContextMenuRequested, this, &QgsLocatorWidget::showContextMenu ); + connect( mModelBridge, &QgsLocatorModelBridge::resultAdded, this, &QgsLocatorWidget::resultAdded ); connect( mModelBridge, &QgsLocatorModelBridge::isRunningChanged, this, [ = ]() {mLineEdit->setShowSpinner( mModelBridge->isRunning() );} ); connect( mModelBridge, & QgsLocatorModelBridge::resultsCleared, this, [ = ]() {mHasSelectedResult = false;} ); @@ -172,6 +175,25 @@ void QgsLocatorWidget::resultAdded() } } +void QgsLocatorWidget::showContextMenu( const QPoint &point ) +{ + QModelIndex index = mResultsView->indexAt( point ); + if ( !index.isValid() ) + return; + + const QList actions = mResultsView->model()->data( index, QgsLocatorModel::ResultActionsRole ).value>(); + QMenu *contextMenu = new QMenu( mResultsView ); + for ( auto resultAction : actions ) + { + QAction *menuAction = new QAction( resultAction.text, contextMenu ); + if ( !resultAction.iconPath.isEmpty() ) + menuAction->setIcon( QIcon( resultAction.iconPath ) ); + connect( menuAction, &QAction::triggered, this, [ = ]() {mModelBridge->triggerResult( index, resultAction.id );} ); + contextMenu->addAction( menuAction ); + } + contextMenu->exec( mResultsView->viewport()->mapToGlobal( point ) ); +} + void QgsLocatorWidget::performSearch() { mPopupTimer.stop(); diff --git a/src/gui/locator/qgslocatorwidget.h b/src/gui/locator/qgslocatorwidget.h index d86ce63fc2a..b90eaa69174 100644 --- a/src/gui/locator/qgslocatorwidget.h +++ b/src/gui/locator/qgslocatorwidget.h @@ -94,6 +94,7 @@ class GUI_EXPORT QgsLocatorWidget : public QWidget void configMenuAboutToShow(); void scheduleDelayedPopup(); void resultAdded(); + void showContextMenu( const QPoint &point ); private: QgsLocatorModelBridge *mModelBridge = nullptr; diff --git a/tests/code_layout/sipifyheader.expected.sip b/tests/code_layout/sipifyheader.expected.sip index acb4e2eb710..24de31a91dd 100644 --- a/tests/code_layout/sipifyheader.expected.sip +++ b/tests/code_layout/sipifyheader.expected.sip @@ -93,6 +93,7 @@ typedef QtClass QtClassQVariantBase; QString mName; int mCount; QgsMapLayer *mLayer; + QList contextMenuActions; }; static const int MONTHS; diff --git a/tests/code_layout/sipifyheader.h b/tests/code_layout/sipifyheader.h index 9d610d52773..f594ba76a5e 100644 --- a/tests/code_layout/sipifyheader.h +++ b/tests/code_layout/sipifyheader.h @@ -134,6 +134,7 @@ class CORE_EXPORT QgsSipifyHeader : public QtClass, private Ui::QgsBas QString mName; int mCount = 100; QgsMapLayer *mLayer = nullptr; + QList contextMenuActions = QList(); }; static const int MONTHS = 60 * 60 * 24 * 30; // something