diff --git a/python/core/qgsaction.sip b/python/core/qgsaction.sip index a8c816d71f7..a52a65c7af5 100644 --- a/python/core/qgsaction.sip +++ b/python/core/qgsaction.sip @@ -44,7 +44,7 @@ class QgsAction \param capture If this is set to true, the output will be captured when an action is run %End - QgsAction( ActionType type, const QString &description, const QString &action, const QString &icon, bool capture, const QString &shortTitle = QString(), const QSet &actionScopes = QSet() ); + QgsAction( ActionType type, const QString &description, const QString &action, const QString &icon, bool capture, const QString &shortTitle = QString(), const QSet &actionScopes = QSet(), const QString ¬ificationMessage = QString() ); %Docstring Create a new QgsAction @@ -55,6 +55,7 @@ class QgsAction \param capture If this is set to true, the output will be captured when an action is run \param shortTitle A short string used to label user interface elements like buttons \param actionScopes A set of scopes in which this action will be available + \param notificationMessage A particular message which reception will trigger the action %End QString name() const; @@ -103,6 +104,14 @@ The icon How the content is interpreted depends on the type() and the actionScope(). +.. versionadded:: 3.0 + :rtype: str +%End + + QString notificationMessage() const; +%Docstring + Returns the notification message that triggers the action + .. versionadded:: 3.0 :rtype: str %End diff --git a/python/core/qgsactionmanager.sip b/python/core/qgsactionmanager.sip index c9b9b78a448..834c0021e34 100644 --- a/python/core/qgsactionmanager.sip +++ b/python/core/qgsactionmanager.sip @@ -12,7 +12,7 @@ -class QgsActionManager +class QgsActionManager: QObject { %Docstring Storage and management of actions associated with a layer. diff --git a/python/core/qgsdataprovider.sip b/python/core/qgsdataprovider.sip index 2520a007545..a312d0e1674 100644 --- a/python/core/qgsdataprovider.sip +++ b/python/core/qgsdataprovider.sip @@ -361,6 +361,18 @@ Current time stamp of data source :rtype: QVariant %End + virtual void setListening( bool isListening ); +%Docstring + Set whether the provider will listen to datasource notifications + If set, the provider will issue notify signals. + + The default implementation does nothing. + +.. seealso:: notify + +.. versionadded:: 3.0 +%End + signals: void fullExtentCalculated(); @@ -379,6 +391,16 @@ Current time stamp of data source feature ids should be invalidated. %End + void notify( const QString &msg ) const; +%Docstring + Emitted when datasource issues a notification + +.. seealso:: setListening + +.. versionadded:: 3.0 +%End + + protected: diff --git a/python/core/qgsexpressioncontext.sip b/python/core/qgsexpressioncontext.sip index 0258f358e7f..7975b54f268 100644 --- a/python/core/qgsexpressioncontext.sip +++ b/python/core/qgsexpressioncontext.sip @@ -940,6 +940,13 @@ class QgsExpressionContextUtils :rtype: QgsExpressionContextScope %End + static QgsExpressionContextScope *notificationScope( const QString &message = QString() ) /Factory/; +%Docstring + Creates a new scope which contains variables and functions relating to provider notifications + \param message the notification message + :rtype: QgsExpressionContextScope +%End + static void registerContextFunctions(); %Docstring Registers all known core functions provided by QgsExpressionContextScope objects. diff --git a/python/core/qgsmaplayer.sip b/python/core/qgsmaplayer.sip index e6e45cdebaa..d0db641a38b 100644 --- a/python/core/qgsmaplayer.sip +++ b/python/core/qgsmaplayer.sip @@ -953,6 +953,39 @@ Time stamp of data source in the moment when data/metadata were loaded by provid :rtype: bool %End + void setRefreshOnNotifyEnabled( bool enabled ); +%Docstring + Set whether provider notification is connected to triggerRepaint + +.. versionadded:: 3.0 +%End + + void setRefreshOnNofifyMessage( const QString &message ); +%Docstring + Set the notification message that triggers repaine + If refresh on notification is enabled, the notification will triggerRepaint only + if the notification message is equal to \param message + +.. versionadded:: 3.0 +%End + + QString refreshOnNotifyMessage() const; +%Docstring + Returns the message that should be notified by the provider to triggerRepaint + +.. versionadded:: 3.0 + :rtype: str +%End + + bool isRefreshOnNotifyEnabled() const; +%Docstring + Returns true if the refresh on provider nofification is enabled + +.. versionadded:: 3.0 + :rtype: bool +%End + + signals: void statusChanged( const QString &status ); @@ -1134,6 +1167,7 @@ Checks whether a new set of dependencies will introduce a cycle :rtype: bool %End + }; diff --git a/src/app/qgsattributeactiondialog.cpp b/src/app/qgsattributeactiondialog.cpp index e9d84649688..6c489740485 100644 --- a/src/app/qgsattributeactiondialog.cpp +++ b/src/app/qgsattributeactiondialog.cpp @@ -145,13 +145,16 @@ void QgsAttributeActionDialog::insertRow( int row, const QgsAction &action ) headerItem->setData( Qt::UserRole, action.iconPath() ); mAttributeActionTable->setVerticalHeaderItem( row, headerItem ); + // Notification message + mAttributeActionTable->setItem( row, NotificationMessage, new QTableWidgetItem( action.notificationMessage() ) ); + updateButtons(); } -void QgsAttributeActionDialog::insertRow( int row, QgsAction::ActionType type, const QString &name, const QString &actionText, const QString &iconPath, bool capture, const QString &shortTitle, const QSet &actionScopes ) +void QgsAttributeActionDialog::insertRow( int row, QgsAction::ActionType type, const QString &name, const QString &actionText, const QString &iconPath, bool capture, const QString &shortTitle, const QSet &actionScopes, const QString ¬ificationMessage ) { if ( uniqueName( name ) == name ) - insertRow( row, QgsAction( type, name, actionText, iconPath, capture, shortTitle, actionScopes ) ); + insertRow( row, QgsAction( type, name, actionText, iconPath, capture, shortTitle, actionScopes, notificationMessage ) ); } void QgsAttributeActionDialog::moveUp() @@ -219,7 +222,9 @@ QgsAction QgsAttributeActionDialog::rowToAction( int row ) const mAttributeActionTable->verticalHeaderItem( row )->data( Qt::UserRole ).toString(), mAttributeActionTable->item( row, Capture )->checkState() == Qt::Checked, mAttributeActionTable->item( row, ShortTitle )->text(), - mAttributeActionTable->item( row, ActionScopes )->data( Qt::UserRole ).value>() ); + mAttributeActionTable->item( row, ActionScopes )->data( Qt::UserRole ).value>(), + mAttributeActionTable->item( row, NotificationMessage )->text() + ); return action; } @@ -273,7 +278,7 @@ void QgsAttributeActionDialog::insert() { QString name = uniqueName( dlg.description() ); - insertRow( pos, dlg.type(), name, dlg.actionText(), dlg.iconPath(), dlg.capture(), dlg.shortTitle(), dlg.actionScopes() ); + insertRow( pos, dlg.type(), name, dlg.actionText(), dlg.iconPath(), dlg.capture(), dlg.shortTitle(), dlg.actionScopes(), dlg.notificationMessage() ); } } @@ -300,14 +305,14 @@ void QgsAttributeActionDialog::updateButtons() void QgsAttributeActionDialog::addDefaultActions() { int pos = 0; - insertRow( pos++, QgsAction::Generic, tr( "Echo attribute's value" ), QStringLiteral( "echo \"[% \"MY_FIELD\" %]\"" ), QLatin1String( "" ), true, tr( "Attribute Value" ), QSet() << QStringLiteral( "Field" ) ); - insertRow( pos++, QgsAction::Generic, tr( "Run an application" ), QStringLiteral( "ogr2ogr -f \"ESRI Shapefile\" \"[% \"OUTPUT_PATH\" %]\" \"[% \"INPUT_FILE\" %]\"" ), QLatin1String( "" ), true, tr( "Run application" ), QSet() << QStringLiteral( "Feature" ) << QStringLiteral( "Canvas" ) ); - insertRow( pos++, QgsAction::GenericPython, tr( "Get feature id" ), QStringLiteral( "from qgis.PyQt import QtWidgets\n\nQtWidgets.QMessageBox.information(None, \"Feature id\", \"feature id is [% $id %]\")" ), QLatin1String( "" ), false, tr( "Feature ID" ), QSet() << QStringLiteral( "Feature" ) << QStringLiteral( "Canvas" ) ); - insertRow( pos++, QgsAction::GenericPython, tr( "Selected field's value (Identify features tool)" ), QStringLiteral( "from qgis.PyQt import QtWidgets\n\nQtWidgets.QMessageBox.information(None, \"Current field's value\", \"[% @current_field %]\")" ), QLatin1String( "" ), false, tr( "Field Value" ), QSet() << QStringLiteral( "Field" ) ); - insertRow( pos++, QgsAction::GenericPython, tr( "Clicked coordinates (Run feature actions tool)" ), QStringLiteral( "from qgis.PyQt import QtWidgets\n\nQtWidgets.QMessageBox.information(None, \"Clicked coords\", \"layer: [% @layer_id %]\\ncoords: ([% @click_x %],[% @click_y %])\")" ), QLatin1String( "" ), false, tr( "Clicked Coordinate" ), QSet() << QStringLiteral( "Canvas" ) ); - insertRow( pos++, QgsAction::OpenUrl, tr( "Open file" ), QStringLiteral( "[% \"PATH\" %]" ), QLatin1String( "" ), false, tr( "Open file" ), QSet() << QStringLiteral( "Feature" ) << QStringLiteral( "Canvas" ) ); - insertRow( pos++, QgsAction::OpenUrl, tr( "Search on web based on attribute's value" ), QStringLiteral( "http://www.google.com/search?q=[% \"ATTRIBUTE\" %]" ), QLatin1String( "" ), false, tr( "Search Web" ), QSet() << QStringLiteral( "Field" ) ); - insertRow( pos++, QgsAction::GenericPython, tr( "List feature ids" ), QStringLiteral( "from qgis.PyQt import QtWidgets\n\nlayer = QgsProject.instance().mapLayer('[% @layer_id %]')\nif layer.selectedFeatureCount():\n ids = layer.selectedFeatureIds()\nelse:\n ids = [f.id() for f in layer.getFeatures()]\n\nQtWidgets.QMessageBox.information(None, \"Feature ids\", ', '.join([str(id) for id in ids]))" ), QLatin1String( "" ), false, tr( "List feature ids" ), QSet() << QStringLiteral( "Layer" ) ); + insertRow( pos++, QgsAction::Generic, tr( "Echo attribute's value" ), QStringLiteral( "echo \"[% \"MY_FIELD\" %]\"" ), QLatin1String( "" ), true, tr( "Attribute Value" ), QSet() << QStringLiteral( "Field" ), QString() ); + insertRow( pos++, QgsAction::Generic, tr( "Run an application" ), QStringLiteral( "ogr2ogr -f \"ESRI Shapefile\" \"[% \"OUTPUT_PATH\" %]\" \"[% \"INPUT_FILE\" %]\"" ), QLatin1String( "" ), true, tr( "Run application" ), QSet() << QStringLiteral( "Feature" ) << QStringLiteral( "Canvas" ), QString() ); + insertRow( pos++, QgsAction::GenericPython, tr( "Get feature id" ), QStringLiteral( "from qgis.PyQt import QtWidgets\n\nQtWidgets.QMessageBox.information(None, \"Feature id\", \"feature id is [% $id %]\")" ), QLatin1String( "" ), false, tr( "Feature ID" ), QSet() << QStringLiteral( "Feature" ) << QStringLiteral( "Canvas" ), QString() ); + insertRow( pos++, QgsAction::GenericPython, tr( "Selected field's value (Identify features tool)" ), QStringLiteral( "from qgis.PyQt import QtWidgets\n\nQtWidgets.QMessageBox.information(None, \"Current field's value\", \"[% @current_field %]\")" ), QLatin1String( "" ), false, tr( "Field Value" ), QSet() << QStringLiteral( "Field" ), QString() ); + insertRow( pos++, QgsAction::GenericPython, tr( "Clicked coordinates (Run feature actions tool)" ), QStringLiteral( "from qgis.PyQt import QtWidgets\n\nQtWidgets.QMessageBox.information(None, \"Clicked coords\", \"layer: [% @layer_id %]\\ncoords: ([% @click_x %],[% @click_y %])\")" ), QLatin1String( "" ), false, tr( "Clicked Coordinate" ), QSet() << QStringLiteral( "Canvas" ), QString() ); + insertRow( pos++, QgsAction::OpenUrl, tr( "Open file" ), QStringLiteral( "[% \"PATH\" %]" ), QLatin1String( "" ), false, tr( "Open file" ), QSet() << QStringLiteral( "Feature" ) << QStringLiteral( "Canvas" ), QString() ); + insertRow( pos++, QgsAction::OpenUrl, tr( "Search on web based on attribute's value" ), QStringLiteral( "http://www.google.com/search?q=[% \"ATTRIBUTE\" %]" ), QLatin1String( "" ), false, tr( "Search Web" ), QSet() << QStringLiteral( "Field" ), QString() ); + insertRow( pos++, QgsAction::GenericPython, tr( "List feature ids" ), QStringLiteral( "from qgis.PyQt import QtWidgets\n\nlayer = QgsProject.instance().mapLayer('[% @layer_id %]')\nif layer.selectedFeatureCount():\n ids = layer.selectedFeatureIds()\nelse:\n ids = [f.id() for f in layer.getFeatures()]\n\nQtWidgets.QMessageBox.information(None, \"Feature ids\", ', '.join([str(id) for id in ids]))" ), QLatin1String( "" ), false, tr( "List feature ids" ), QSet() << QStringLiteral( "Layer" ), QString() ); } void QgsAttributeActionDialog::itemDoubleClicked( QTableWidgetItem *item ) @@ -322,6 +327,7 @@ void QgsAttributeActionDialog::itemDoubleClicked( QTableWidgetItem *item ) mAttributeActionTable->item( row, ActionText )->text(), mAttributeActionTable->item( row, Capture )->checkState() == Qt::Checked, mAttributeActionTable->item( row, ActionScopes )->data( Qt::UserRole ).value>(), + mAttributeActionTable->item( row, NotificationMessage )->text(), mLayer ); @@ -335,6 +341,7 @@ void QgsAttributeActionDialog::itemDoubleClicked( QTableWidgetItem *item ) mAttributeActionTable->item( row, ShortTitle )->setText( actionProperties.shortTitle() ); mAttributeActionTable->item( row, ActionText )->setText( actionProperties.actionText() ); mAttributeActionTable->item( row, Capture )->setCheckState( actionProperties.capture() ? Qt::Checked : Qt::Unchecked ); + mAttributeActionTable->item( row, NotificationMessage )->setText( actionProperties.notificationMessage() ); QTableWidgetItem *item = mAttributeActionTable->item( row, ActionScopes ); QStringList actionScopes = actionProperties.actionScopes().toList(); diff --git a/src/app/qgsattributeactiondialog.h b/src/app/qgsattributeactiondialog.h index c2ca921cda3..72539321b3d 100644 --- a/src/app/qgsattributeactiondialog.h +++ b/src/app/qgsattributeactiondialog.h @@ -43,7 +43,8 @@ class APP_EXPORT QgsAttributeActionDialog: public QWidget, private Ui::QgsAttrib ShortTitle, ActionText, Capture, - ActionScopes + ActionScopes, + NotificationMessage }; public: @@ -69,7 +70,7 @@ class APP_EXPORT QgsAttributeActionDialog: public QWidget, private Ui::QgsAttrib private: void insertRow( int row, const QgsAction &action ); - void insertRow( int row, QgsAction::ActionType type, const QString &name, const QString &actionText, const QString &iconPath, bool capture, const QString &shortTitle, const QSet &actionScopes ); + void insertRow( int row, QgsAction::ActionType type, const QString &name, const QString &actionText, const QString &iconPath, bool capture, const QString &shortTitle, const QSet &actionScopes, const QString ¬ificationMessage ); void swapRows( int row1, int row2 ); QgsAction rowToAction( int row ) const; diff --git a/src/app/qgsattributeactionpropertiesdialog.cpp b/src/app/qgsattributeactionpropertiesdialog.cpp index fa0d8737d99..bbee988d6fd 100644 --- a/src/app/qgsattributeactionpropertiesdialog.cpp +++ b/src/app/qgsattributeactionpropertiesdialog.cpp @@ -31,7 +31,7 @@ #include #include -QgsAttributeActionPropertiesDialog::QgsAttributeActionPropertiesDialog( QgsAction::ActionType type, const QString &description, const QString &shortTitle, const QString &iconPath, const QString &actionText, bool capture, const QSet &actionScopes, QgsVectorLayer *layer, QWidget *parent ) +QgsAttributeActionPropertiesDialog::QgsAttributeActionPropertiesDialog( QgsAction::ActionType type, const QString &description, const QString &shortTitle, const QString &iconPath, const QString &actionText, bool capture, const QSet &actionScopes, const QString ¬ificationMessage, QgsVectorLayer *layer, QWidget *parent ) : QDialog( parent ) , mLayer( layer ) { @@ -44,6 +44,7 @@ QgsAttributeActionPropertiesDialog::QgsAttributeActionPropertiesDialog( QgsActio mIconPreview->setPixmap( QPixmap( iconPath ) ); mActionText->setText( actionText ); mCaptureOutput->setChecked( capture ); + mNotificationMessage->setText( notificationMessage ); init( actionScopes ); } @@ -101,6 +102,12 @@ QSet QgsAttributeActionPropertiesDialog::actionScopes() const return actionScopes; } +QString QgsAttributeActionPropertiesDialog::notificationMessage() const +{ + return mNotificationMessage->text(); +} + + bool QgsAttributeActionPropertiesDialog::capture() const { return mCaptureOutput->isChecked(); @@ -119,6 +126,8 @@ QgsExpressionContext QgsAttributeActionPropertiesDialog::createExpressionContext } } + context << QgsExpressionContextUtils::notificationScope(); + return context; } diff --git a/src/app/qgsattributeactionpropertiesdialog.h b/src/app/qgsattributeactionpropertiesdialog.h index aa1075ddf40..c638af4f133 100644 --- a/src/app/qgsattributeactionpropertiesdialog.h +++ b/src/app/qgsattributeactionpropertiesdialog.h @@ -28,7 +28,7 @@ class QgsAttributeActionPropertiesDialog: public QDialog, private Ui::QgsAttribu Q_OBJECT public: - QgsAttributeActionPropertiesDialog( QgsAction::ActionType type, const QString &description, const QString &shortTitle, const QString &iconPath, const QString &actionText, bool capture, const QSet &actionScopes, QgsVectorLayer *layer, QWidget *parent = nullptr ); + QgsAttributeActionPropertiesDialog( QgsAction::ActionType type, const QString &description, const QString &shortTitle, const QString &iconPath, const QString &actionText, bool capture, const QSet &actionScopes, const QString ¬ificationMessage, QgsVectorLayer *layer, QWidget *parent = nullptr ); QgsAttributeActionPropertiesDialog( QgsVectorLayer *layer, QWidget *parent = nullptr ); @@ -44,6 +44,8 @@ class QgsAttributeActionPropertiesDialog: public QDialog, private Ui::QgsAttribu QSet actionScopes() const; + QString notificationMessage() const; + bool capture() const; virtual QgsExpressionContext createExpressionContext() const override; diff --git a/src/app/qgsvectorlayerproperties.cpp b/src/app/qgsvectorlayerproperties.cpp index 3237ee41bb9..15a3c056dd4 100644 --- a/src/app/qgsvectorlayerproperties.cpp +++ b/src/app/qgsvectorlayerproperties.cpp @@ -454,6 +454,11 @@ void QgsVectorLayerProperties::syncToLayer() mRefreshLayerIntervalSpinBox->setEnabled( mLayer->hasAutoRefreshEnabled() ); mRefreshLayerIntervalSpinBox->setValue( mLayer->autoRefreshInterval() / 1000.0 ); + mRefreshLayerNotificationCheckBox->setChecked( mLayer->isRefreshOnNotifyEnabled() ); + mNotificationMessageCheckBox->setChecked( !mLayer->refreshOnNotifyMessage().isEmpty() ); + mNotifyMessagValueLineEdit->setText( mLayer->refreshOnNotifyMessage() ); + + // load appropriate symbology page (V1 or V2) updateSymbologyPage(); @@ -632,6 +637,9 @@ void QgsVectorLayerProperties::apply() mLayer->setAutoRefreshInterval( mRefreshLayerIntervalSpinBox->value() * 1000.0 ); mLayer->setAutoRefreshEnabled( mRefreshLayerCheckBox->isChecked() ); + mLayer->setRefreshOnNotifyEnabled( mRefreshLayerNotificationCheckBox->isChecked() ); + mLayer->setRefreshOnNofifyMessage( mNotificationMessageCheckBox->isChecked() ? mNotifyMessagValueLineEdit->text() : QString() ); + mOldJoins = mLayer->vectorJoins(); //save variables diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index c1434f9880f..a3b38d79426 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -562,6 +562,7 @@ ENDIF(NOT MSVC) SET(QGIS_CORE_MOC_HDRS qgsapplication.h + qgsactionmanager.h qgsactionscoperegistry.h qgsanimatedicon.h qgsbrowsermodel.h diff --git a/src/core/expression/qgsexpression.cpp b/src/core/expression/qgsexpression.cpp index 55162ad6d6a..3069dca884d 100644 --- a/src/core/expression/qgsexpression.cpp +++ b/src/core/expression/qgsexpression.cpp @@ -703,6 +703,9 @@ void QgsExpression::initVariableHelp() //processing variables sVariableHelpTexts.insert( QStringLiteral( "algorithm_id" ), QCoreApplication::translate( "algorithm_id", "Unique ID for algorithm." ) ); + + //provider notification + sVariableHelpTexts.insert( QStringLiteral( "notification_message" ), QCoreApplication::translate( "notification_message", "Contend of the notification message sent by the provider (available only for actions triggered by provider notifications)." ) ); } QString QgsExpression::variableHelpText( const QString &variableName ) diff --git a/src/core/qgsaction.cpp b/src/core/qgsaction.cpp index a482effa927..827d749b5db 100644 --- a/src/core/qgsaction.cpp +++ b/src/core/qgsaction.cpp @@ -119,6 +119,7 @@ void QgsAction::readXml( const QDomNode &actionNode ) mIcon = actionElement.attributeNode( QStringLiteral( "icon" ) ).value(); mCaptureOutput = actionElement.attributeNode( QStringLiteral( "capture" ) ).value().toInt() != 0; mShortTitle = actionElement.attributeNode( QStringLiteral( "shortTitle" ) ).value(); + mNotificationMessage = actionElement.attributeNode( QStringLiteral( "notificationMessage" ) ).value(); mId = QUuid( actionElement.attributeNode( QStringLiteral( "id" ) ).value() ); if ( mId.isNull() ) mId = QUuid::createUuid(); @@ -133,6 +134,7 @@ void QgsAction::writeXml( QDomNode &actionsNode ) const actionSetting.setAttribute( QStringLiteral( "icon" ), mIcon ); actionSetting.setAttribute( QStringLiteral( "action" ), mCommand ); actionSetting.setAttribute( QStringLiteral( "capture" ), mCaptureOutput ); + actionSetting.setAttribute( QStringLiteral( "notificationMessage" ), mNotificationMessage ); actionSetting.setAttribute( QStringLiteral( "id" ), mId.toString() ); Q_FOREACH ( const QString &scope, mActionScopes ) diff --git a/src/core/qgsaction.h b/src/core/qgsaction.h index 7f4be7fe660..3ab1eba610c 100644 --- a/src/core/qgsaction.h +++ b/src/core/qgsaction.h @@ -77,8 +77,9 @@ class CORE_EXPORT QgsAction * \param capture If this is set to true, the output will be captured when an action is run * \param shortTitle A short string used to label user interface elements like buttons * \param actionScopes A set of scopes in which this action will be available + * \param notificationMessage A particular message which reception will trigger the action */ - QgsAction( ActionType type, const QString &description, const QString &action, const QString &icon, bool capture, const QString &shortTitle = QString(), const QSet &actionScopes = QSet() ) + QgsAction( ActionType type, const QString &description, const QString &action, const QString &icon, bool capture, const QString &shortTitle = QString(), const QSet &actionScopes = QSet(), const QString ¬ificationMessage = QString() ) : mType( type ) , mDescription( description ) , mShortTitle( shortTitle ) @@ -86,6 +87,7 @@ class CORE_EXPORT QgsAction , mCommand( action ) , mCaptureOutput( capture ) , mActionScopes( actionScopes ) + , mNotificationMessage( notificationMessage ) , mId( QUuid::createUuid() ) {} @@ -124,6 +126,13 @@ class CORE_EXPORT QgsAction */ QString command() const { return mCommand; } + /** + * Returns the notification message that triggers the action + * + * \since QGIS 3.0 + */ + QString notificationMessage() const { return mNotificationMessage; } + //! The action type ActionType type() const { return mType; } @@ -190,6 +199,7 @@ class CORE_EXPORT QgsAction QString mCommand; bool mCaptureOutput = false; QSet mActionScopes; + QString mNotificationMessage; mutable std::shared_ptr mAction; QUuid mId; }; diff --git a/src/core/qgsactionmanager.cpp b/src/core/qgsactionmanager.cpp index 116bb3b5dd1..ac8fa8a8697 100644 --- a/src/core/qgsactionmanager.cpp +++ b/src/core/qgsactionmanager.cpp @@ -29,6 +29,7 @@ #include "qgsproject.h" #include "qgslogger.h" #include "qgsexpression.h" +#include "qgsdataprovider.h" #include #include @@ -38,6 +39,7 @@ #include #include #include +#include QUuid QgsActionManager::addAction( QgsAction::ActionType type, const QString &name, const QString &command, bool capture ) @@ -56,21 +58,69 @@ QUuid QgsActionManager::addAction( QgsAction::ActionType type, const QString &na void QgsActionManager::addAction( const QgsAction &action ) { + QgsDebugMsg( "add action " + action.name() ); mActions.append( action ); + if ( mLayer && mLayer->dataProvider() && !action.notificationMessage().isEmpty() ) + { + mLayer->dataProvider()->setListening( true ); + if ( !mOnNotifyConnected ) + { + QgsDebugMsg( "connecting to notify" ); + connect( mLayer->dataProvider(), &QgsDataProvider::notify, this, &QgsActionManager::onNotifyRunActions ); + mOnNotifyConnected = true; + } + } +} + +void QgsActionManager::onNotifyRunActions( const QString &message ) +{ + for ( const QgsAction &act : qgsAsConst( mActions ) ) + { + if ( !act.notificationMessage().isEmpty() && QRegularExpression( act.notificationMessage() ).match( message ).hasMatch() ) + { + if ( !act.isValid() || !act.runable() ) + continue; + + QgsExpressionContext context = createExpressionContext(); + + Q_ASSERT( mLayer ); // if there is no layer, then where is the notification coming from ? + context << QgsExpressionContextUtils::layerScope( mLayer ); + context << QgsExpressionContextUtils::notificationScope( message ); + + QString expandedAction = QgsExpression::replaceExpressionText( act.command(), &context ); + if ( expandedAction.isEmpty() ) + continue; + runAction( QgsAction( act.type(), act.name(), expandedAction, act.capture() ) ); + } + } } void QgsActionManager::removeAction( const QUuid &actionId ) { int i = 0; - Q_FOREACH ( const QgsAction &action, mActions ) + for ( const QgsAction &action : qgsAsConst( mActions ) ) { if ( action.id() == actionId ) { mActions.removeAt( i ); - return; + break; } ++i; } + + if ( mOnNotifyConnected ) + { + bool hasActionOnNotify = false; + for ( const QgsAction &action : qgsAsConst( mActions ) ) + hasActionOnNotify |= !action.notificationMessage().isEmpty(); + if ( !hasActionOnNotify && mLayer && mLayer->dataProvider() ) + { + // note that there is no way of knowing if the provider is listening only because + // this class has hasked it to, so we do not reset the provider listening state here + disconnect( mLayer->dataProvider(), &QgsDataProvider::notify, this, &QgsActionManager::onNotifyRunActions ); + mOnNotifyConnected = false; + } + } } void QgsActionManager::doAction( const QUuid &actionId, const QgsFeature &feature, int defaultValueIndex ) @@ -109,6 +159,13 @@ void QgsActionManager::doAction( const QUuid &actionId, const QgsFeature &feat, void QgsActionManager::clearActions() { mActions.clear(); + if ( mOnNotifyConnected && mLayer && mLayer->dataProvider() ) + { + // note that there is no way of knowing if the provider is listening only because + // this class has hasked it to, so we do not reset the provider listening state here + disconnect( mLayer->dataProvider(), &QgsDataProvider::notify, this, &QgsActionManager::onNotifyRunActions ); + mOnNotifyConnected = false; + } } QList QgsActionManager::actions( const QString &actionScope ) const @@ -119,7 +176,7 @@ QList QgsActionManager::actions( const QString &actionScope ) const { QList actions; - Q_FOREACH ( const QgsAction &action, mActions ) + for ( const QgsAction &action : qgsAsConst( mActions ) ) { if ( action.actionScopes().contains( actionScope ) ) actions.append( action ); @@ -174,7 +231,7 @@ bool QgsActionManager::writeXml( QDomNode &layer_node ) const aActions.appendChild( defaultActionElement ); } - Q_FOREACH ( const QgsAction &action, mActions ) + for ( const QgsAction &action : qgsAsConst( mActions ) ) { action.writeXml( aActions ); } @@ -185,7 +242,7 @@ bool QgsActionManager::writeXml( QDomNode &layer_node ) const bool QgsActionManager::readXml( const QDomNode &layer_node ) { - mActions.clear(); + clearActions(); QDomNode aaNode = layer_node.namedItem( QStringLiteral( "attributeactions" ) ); @@ -196,7 +253,7 @@ bool QgsActionManager::readXml( const QDomNode &layer_node ) { QgsAction action; action.readXml( actionsettings.item( i ) ); - mActions.append( action ); + addAction( action ); } QDomNodeList defaultActionNodes = aaNode.toElement().elementsByTagName( "defaultAction" ); @@ -212,7 +269,7 @@ bool QgsActionManager::readXml( const QDomNode &layer_node ) QgsAction QgsActionManager::action( const QUuid &id ) { - Q_FOREACH ( const QgsAction &action, mActions ) + for ( const QgsAction &action : qgsAsConst( mActions ) ) { if ( action.id() == id ) return action; diff --git a/src/core/qgsactionmanager.h b/src/core/qgsactionmanager.h index 2dfbe263839..7cf036db5cd 100644 --- a/src/core/qgsactionmanager.h +++ b/src/core/qgsactionmanager.h @@ -27,6 +27,7 @@ #include "qgis_core.h" #include #include +#include #include "qgsaction.h" #include "qgsfeature.h" @@ -46,8 +47,10 @@ class QgsExpressionContext; * based on attributes of a given feature. */ -class CORE_EXPORT QgsActionManager +class CORE_EXPORT QgsActionManager: public QObject { + Q_OBJECT + public: //! Constructor QgsActionManager( QgsVectorLayer *layer ) @@ -148,7 +151,12 @@ class CORE_EXPORT QgsActionManager QMap mDefaultActions; + bool mOnNotifyConnected = false; + QgsExpressionContext createExpressionContext() const; + + private slots: + void onNotifyRunActions( const QString &message ); }; #endif diff --git a/src/core/qgsdataprovider.cpp b/src/core/qgsdataprovider.cpp index 1a3d24622ce..fd68008c4a7 100644 --- a/src/core/qgsdataprovider.cpp +++ b/src/core/qgsdataprovider.cpp @@ -36,3 +36,8 @@ QVariant QgsDataProvider::providerProperty( int property, const QVariant &defaul return mProviderProperties.value( property, defaultValue ); } +void QgsDataProvider::setListening( bool isListening ) +{ + Q_UNUSED( isListening ); +} + diff --git a/src/core/qgsdataprovider.h b/src/core/qgsdataprovider.h index a0327cb1f85..ac6a993f6bd 100644 --- a/src/core/qgsdataprovider.h +++ b/src/core/qgsdataprovider.h @@ -429,6 +429,18 @@ class CORE_EXPORT QgsDataProvider : public QObject */ QVariant providerProperty( int property, const QVariant &defaultValue ) const; // SIP_SKIP + /** + * Set whether the provider will listen to datasource notifications + * If set, the provider will issue notify signals. + * + * The default implementation does nothing. + * + * \see notify + * + * \since QGIS 3.0 + */ + virtual void setListening( bool isListening ); + signals: /** @@ -447,6 +459,16 @@ class CORE_EXPORT QgsDataProvider : public QObject */ void dataChanged(); + /** + * Emitted when datasource issues a notification + * + * \see setListening + * + * \since QGIS 3.0 + */ + void notify( const QString &msg ) const; + + protected: /** diff --git a/src/core/qgsexpressioncontext.cpp b/src/core/qgsexpressioncontext.cpp index 56936aebdd7..2c22a7b69a7 100644 --- a/src/core/qgsexpressioncontext.cpp +++ b/src/core/qgsexpressioncontext.cpp @@ -1230,6 +1230,13 @@ QgsExpressionContextScope *QgsExpressionContextUtils::processingAlgorithmScope( return scope.release(); } +QgsExpressionContextScope *QgsExpressionContextUtils::notificationScope( const QString &message ) +{ + std::unique_ptr< QgsExpressionContextScope > scope( new QgsExpressionContextScope() ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "notification_message" ), message, true ) ); + return scope.release(); +} + void QgsExpressionContextUtils::registerContextFunctions() { QgsExpression::registerFunction( new GetNamedProjectColor( nullptr ) ); diff --git a/src/core/qgsexpressioncontext.h b/src/core/qgsexpressioncontext.h index b9a9e7e173a..b47e9080502 100644 --- a/src/core/qgsexpressioncontext.h +++ b/src/core/qgsexpressioncontext.h @@ -861,6 +861,12 @@ class CORE_EXPORT QgsExpressionContextUtils */ static QgsExpressionContextScope *processingAlgorithmScope( const QgsProcessingAlgorithm *algorithm, const QVariantMap ¶meters, QgsProcessingContext &context ) SIP_FACTORY; + /** + * Creates a new scope which contains variables and functions relating to provider notifications + * \param message the notification message + */ + static QgsExpressionContextScope *notificationScope( const QString &message = QString() ) SIP_FACTORY; + /** Registers all known core functions provided by QgsExpressionContextScope objects. */ static void registerContextFunctions(); diff --git a/src/core/qgsmaplayer.cpp b/src/core/qgsmaplayer.cpp index 18544bb1f34..77fbab5b759 100644 --- a/src/core/qgsmaplayer.cpp +++ b/src/core/qgsmaplayer.cpp @@ -472,6 +472,9 @@ bool QgsMapLayer::readLayerXml( const QDomElement &layerElement, const QgsReadWr setAutoRefreshInterval( layerElement.attribute( QStringLiteral( "autoRefreshTime" ), 0 ).toInt() ); setAutoRefreshEnabled( layerElement.attribute( QStringLiteral( "autoRefreshEnabled" ), QStringLiteral( "0" ) ).toInt() ); + setRefreshOnNofifyMessage( layerElement.attribute( QStringLiteral( "refreshOnNotifyMessage" ), QString() ) ); + setRefreshOnNotifyEnabled( layerElement.attribute( QStringLiteral( "refreshOnNotifyEnabled" ), QStringLiteral( "0" ) ).toInt() ); + // set name mnl = layerElement.namedItem( QStringLiteral( "layername" ) ); @@ -577,6 +580,9 @@ bool QgsMapLayer::writeLayerXml( QDomElement &layerElement, QDomDocument &docume layerElement.setAttribute( QStringLiteral( "autoRefreshTime" ), QString::number( mRefreshTimer.interval() ) ); layerElement.setAttribute( QStringLiteral( "autoRefreshEnabled" ), mRefreshTimer.isActive() ? 1 : 0 ); + layerElement.setAttribute( QStringLiteral( "refreshOnNotifyEnabled" ), mIsRefreshOnNofifyEnabled ? 1 : 0 ); + layerElement.setAttribute( QStringLiteral( "refreshOnNotifyMessage" ), mRefreshOnNofifyMessage ); + // ID QDomElement layerId = document.createElement( QStringLiteral( "id" ) ); @@ -1775,3 +1781,28 @@ bool QgsMapLayer::setDependencies( const QSet &oDeps ) emit dependenciesChanged(); return true; } + +void QgsMapLayer::setRefreshOnNotifyEnabled( bool enabled ) +{ + if ( !dataProvider() ) + return; + + if ( enabled && !isRefreshOnNotifyEnabled() ) + { + dataProvider()->setListening( enabled ); + connect( dataProvider(), &QgsVectorDataProvider::notify, this, &QgsMapLayer::onNotifiedTriggerRepaint ); + } + else if ( !enabled && isRefreshOnNotifyEnabled() ) + { + // we don't want to disable provider listening because someone else could need it (e.g. actions) + disconnect( dataProvider(), &QgsVectorDataProvider::notify, this, &QgsMapLayer::onNotifiedTriggerRepaint ); + } + mIsRefreshOnNofifyEnabled = enabled; +} + +void QgsMapLayer::onNotifiedTriggerRepaint( const QString &message ) +{ + if ( refreshOnNotifyMessage().isEmpty() || refreshOnNotifyMessage() == message ) + triggerRepaint(); +} + diff --git a/src/core/qgsmaplayer.h b/src/core/qgsmaplayer.h index d2e5f1f10a9..f8f9b51fd36 100644 --- a/src/core/qgsmaplayer.h +++ b/src/core/qgsmaplayer.h @@ -847,6 +847,37 @@ class CORE_EXPORT QgsMapLayer : public QObject */ virtual bool setDependencies( const QSet &layers ); + /** + * Set whether provider notification is connected to triggerRepaint + * + * \since QGIS 3.0 + */ + void setRefreshOnNotifyEnabled( bool enabled ); + + /** + * Set the notification message that triggers repaine + * If refresh on notification is enabled, the notification will triggerRepaint only + * if the notification message is equal to \param message + * + * \since QGIS 3.0 + */ + void setRefreshOnNofifyMessage( const QString &message ) { mRefreshOnNofifyMessage = message; } + + /** + * Returns the message that should be notified by the provider to triggerRepaint + * + * \since QGIS 3.0 + */ + QString refreshOnNotifyMessage() const { return mRefreshOnNofifyMessage; } + + /** + * Returns true if the refresh on provider nofification is enabled + * + * \since QGIS 3.0 + */ + bool isRefreshOnNotifyEnabled() const { return mIsRefreshOnNofifyEnabled; } + + signals: //! Emit a signal with status (e.g. to be caught by QgisApp and display a msg on status bar) @@ -931,6 +962,10 @@ class CORE_EXPORT QgsMapLayer : public QObject */ void metadataChanged(); + private slots: + + void onNotifiedTriggerRepaint( const QString &message ); + protected: /** Copies attributes like name, short name, ... into another layer. @@ -1030,6 +1065,9 @@ class CORE_EXPORT QgsMapLayer : public QObject //! Checks whether a new set of dependencies will introduce a cycle bool hasDependencyCycle( const QSet &layers ) const; + bool mIsRefreshOnNofifyEnabled = false; + QString mRefreshOnNofifyMessage; + private: /** diff --git a/src/core/qgsvectorlayer.cpp b/src/core/qgsvectorlayer.cpp index 675bce79e10..bee1dee89f0 100644 --- a/src/core/qgsvectorlayer.cpp +++ b/src/core/qgsvectorlayer.cpp @@ -4434,3 +4434,4 @@ bool QgsVectorLayer::readExtentFromXml() const { return mReadExtentFromXml; } + diff --git a/src/providers/postgres/CMakeLists.txt b/src/providers/postgres/CMakeLists.txt index 2a8f76d3d32..197d5a62994 100644 --- a/src/providers/postgres/CMakeLists.txt +++ b/src/providers/postgres/CMakeLists.txt @@ -11,6 +11,7 @@ SET(PG_SRCS qgspgtablemodel.cpp qgscolumntypethread.cpp qgspostgresexpressioncompiler.cpp + qgspostgreslistener.cpp ) SET(PG_MOC_HDRS @@ -21,7 +22,7 @@ SET(PG_MOC_HDRS qgspostgresdataitems.h qgspostgresprovider.h qgspostgrestransaction.h - + qgspostgreslistener.h ) IF (WITH_GUI) diff --git a/src/providers/postgres/qgspostgreslistener.cpp b/src/providers/postgres/qgspostgreslistener.cpp new file mode 100644 index 00000000000..3a9c35366a6 --- /dev/null +++ b/src/providers/postgres/qgspostgreslistener.cpp @@ -0,0 +1,127 @@ +/*************************************************************************** + qgspostgreslistener.cpp - Listen to postgres NOTIFY + ------------------- + begin : Sept 11, 2017 + copyright : (C) 2017 by Vincent Mora + email : vincent dor mora at oslandia 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 "qgspostgreslistener.h" + +#include "qgslogger.h" + +#ifdef Q_OS_WIN +#include +#else +#include +#endif + +extern "C" +{ +#include +} + +std::unique_ptr< QgsPostgresListener > QgsPostgresListener::create( const QString &connString ) +{ + std::unique_ptr< QgsPostgresListener > res( new QgsPostgresListener( connString ) ); + QgsDebugMsg( "starting notification listener" ); + res->start(); + res->mMutex.lock(); + res->mIsReadyCondition.wait( &res->mMutex ); + res->mMutex.unlock(); + + return res; +} + +QgsPostgresListener::QgsPostgresListener( const QString &connString ) + : mConnString( connString ) +{ +} + +QgsPostgresListener::~QgsPostgresListener() +{ + mStop = true; + QgsDebugMsg( "stopping the loop" ); + wait(); + QgsDebugMsg( "notification listener stopped" ); +} + +void QgsPostgresListener::run() +{ + PGconn *conn; + conn = PQconnectdb( mConnString.toLocal8Bit() ); + + PGresult *res = PQexec( conn, "LISTEN qgis" ); + if ( PQresultStatus( res ) != PGRES_COMMAND_OK ) + { + QgsDebugMsg( "error in listen" ); + PQclear( res ); + PQfinish( conn ); + mMutex.lock(); + mIsReadyCondition.wakeOne(); + mMutex.unlock(); + return; + } + PQclear( res ); + mMutex.lock(); + mIsReadyCondition.wakeOne(); + mMutex.unlock(); + + const int sock = PQsocket( conn ); + if ( sock < 0 ) + { + QgsDebugMsg( "error in socket" ); + PQfinish( conn ); + return; + } + + forever + { + fd_set input_mask; + FD_ZERO( &input_mask ); + FD_SET( sock, &input_mask ); + + timeval timeout; + timeout.tv_sec = 1; + timeout.tv_usec = 0; + + QgsDebugMsg( "select in the loop" ); + if ( select( sock + 1, &input_mask, nullptr, nullptr, &timeout ) < 0 ) + { + QgsDebugMsg( "error in select" ); + break; + } + + PQconsumeInput( conn ); + PGnotify *n = PQnotifies( conn ); + if ( n ) + { + const QString msg( n->extra ); + emit notify( msg ); + QgsDebugMsg( "notify " + msg ); + PQfreemem( n ); + } + else + { + QgsDebugMsg( "not a notify" ); + } + + if ( mStop ) + { + QgsDebugMsg( "stop from main thread" ); + break; + } + } + PQfinish( conn ); +} + + diff --git a/src/providers/postgres/qgspostgreslistener.h b/src/providers/postgres/qgspostgreslistener.h new file mode 100644 index 00000000000..cc8f2393c1d --- /dev/null +++ b/src/providers/postgres/qgspostgreslistener.h @@ -0,0 +1,65 @@ +/*************************************************************************** + qgspostgreslistener.h - Listen to postgres NOTIFY + ------------------- + begin : Sept 11, 2017 + copyright : (C) 2017 by Vincent Mora + email : vincent dor mora at oslandia 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. * + * * + ***************************************************************************/ + +#ifndef QGSPOSTGRESLISTENER_H +#define QGSPOSTGRESLISTENER_H + +#include + +#include +#include +#include + +/** + * \class QgsPostgresListener + * \brief Launch a thread to listen on postgres notifications on the "qgis" channel, the notify signal is emitted on postgres notify. + * + * \since QGIS 3.0 + */ + +class QgsPostgresListener : public QThread +{ + Q_OBJECT + + public: + + /** + * create an instance if possible and starts the associated thread + * /returns nullptr on error + */ + static std::unique_ptr< QgsPostgresListener > create( const QString &connString ); + + ~QgsPostgresListener(); + + void run() override; + + signals: + void notify( QString message ); + + private: + volatile bool mStop = false; + const QString mConnString; + QWaitCondition mIsReadyCondition; + QMutex mMutex; + + QgsPostgresListener( const QString &connString ); + + Q_DISABLE_COPY( QgsPostgresListener ) + +}; + +#endif // QGSPOSTGRESLISTENER_H diff --git a/src/providers/postgres/qgspostgresprovider.cpp b/src/providers/postgres/qgspostgresprovider.cpp index f5334a826fa..7ddf3880298 100644 --- a/src/providers/postgres/qgspostgresprovider.cpp +++ b/src/providers/postgres/qgspostgresprovider.cpp @@ -35,6 +35,7 @@ #include "qgspostgresdataitems.h" #include "qgspostgresfeatureiterator.h" #include "qgspostgrestransaction.h" +#include "qgspostgreslistener.h" #include "qgslogger.h" #include "qgsfeedback.h" #include "qgssettings.h" @@ -309,6 +310,20 @@ QgsPostgresConn *QgsPostgresProvider::connectionRO() const return mTransaction ? mTransaction->connection() : mConnectionRO; } +void QgsPostgresProvider::setListening( bool isListening ) +{ + if ( isListening && !mListener ) + { + mListener.reset( QgsPostgresListener::create( mUri.connectionInfo( false ) ).release() ); + connect( mListener.get(), &QgsPostgresListener::notify, this, &QgsPostgresProvider::notify ); + } + else if ( !isListening && mListener ) + { + disconnect( mListener.get(), &QgsPostgresListener::notify, this, &QgsPostgresProvider::notify ); + mListener.reset(); + } +} + QgsPostgresConn *QgsPostgresProvider::connectionRW() { if ( mTransaction ) diff --git a/src/providers/postgres/qgspostgresprovider.h b/src/providers/postgres/qgspostgresprovider.h index 75d2f452a63..7a586e8e438 100644 --- a/src/providers/postgres/qgspostgresprovider.h +++ b/src/providers/postgres/qgspostgresprovider.h @@ -32,6 +32,7 @@ class QgsGeometry; class QgsPostgresFeatureIterator; class QgsPostgresSharedData; class QgsPostgresTransaction; +class QgsPostgresListener; #include "qgsdatasourceuri.h" @@ -208,6 +209,14 @@ class QgsPostgresProvider : public QgsVectorDataProvider */ virtual bool hasMetadata() const override; + /** + * Launch a listening thead to listen to postgres NOTIFY on "qgis" channel + * the notification is transformed into a Qt signal. + * + * \since QGIS 3.0 + */ + void setListening( bool isListening ) override; + signals: /** @@ -434,6 +443,8 @@ class QgsPostgresProvider : public QgsVectorDataProvider QHash mDefaultValues; bool mCheckPrimaryKeyUnicity = true; + + std::unique_ptr< QgsPostgresListener > mListener; }; diff --git a/src/ui/qgsattributeactiondialogbase.ui b/src/ui/qgsattributeactiondialogbase.ui index 88ca75a5bb8..2356e1109f2 100644 --- a/src/ui/qgsattributeactiondialogbase.ui +++ b/src/ui/qgsattributeactiondialogbase.ui @@ -6,7 +6,7 @@ 0 0 - 653 + 948 731 @@ -44,7 +44,7 @@ Action list - + actiongroup @@ -146,7 +146,7 @@ QAbstractItemView::SelectRows - 6 + 7 @@ -178,6 +178,14 @@ Action Scopes + + + On Notification + + + <html><head/><body><p>If not empty, this will enable providr notification listening and the action will be executed when hte notification message matched the specified value. </p></body></html> + + diff --git a/src/ui/qgsattributeactionpropertiesdialogbase.ui b/src/ui/qgsattributeactionpropertiesdialogbase.ui index addc885fe3c..ba3bc716fee 100644 --- a/src/ui/qgsattributeactionpropertiesdialogbase.ui +++ b/src/ui/qgsattributeactionpropertiesdialogbase.ui @@ -27,100 +27,6 @@ - - - - - - - 24 - 24 - - - - - 24 - 24 - - - - - - - - - - - - - - - - - - :/images/themes/default/mActionFileOpen.svg:/images/themes/default/mActionFileOpen.svg - - - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok - - - - - - - Type - - - mActionType - - - - - - - - 0 - 0 - - - - - Generic - - - - - Python - - - - - Mac - - - - - Windows - - - - - Unix - - - - - Open - - - - @@ -136,7 +42,7 @@ - + Qt::TabFocus @@ -204,9 +110,156 @@ + + + + + + Execute if notification matches + + + + + + + <html><head/><body><p>If specified, listen to data source notification and performs action if notification message matches the specified value.</p><p>E.g. to match messag beginning with <span style=" font-weight:600;">wathever </span>use <span style=" font-weight:600;">^whatever</span></p></body></html> + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok + + + + + + + Icon + + + + + + + + + + 24 + 24 + + + + + 24 + 24 + + + + + + + + + + + + + + + + + + :/images/themes/default/mActionFileOpen.svg:/images/themes/default/mActionFileOpen.svg + + + + + + + + + Type + + + mActionType + + + + + + + + 0 + 0 + + + + + Generic + + + + + Python + + + + + Mac + + + + + Windows + + + + + Unix + + + + + Open + + + + + + + + Qt::StrongFocus + + + Action Scopes + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + + + + Enter the action name here + + + Enter the name of an action here. The name should be unique (QGIS will make it unique if necessary). + + + Mandatory description + + + @@ -234,37 +287,6 @@ - - - - Icon - - - - - - - Enter the action name here - - - Enter the name of an action here. The name should be unique (QGIS will make it unique if necessary). - - - Mandatory description - - - - - - - Qt::StrongFocus - - - Action Scopes - - - - diff --git a/src/ui/qgsvectorlayerpropertiesbase.ui b/src/ui/qgsvectorlayerpropertiesbase.ui index d62b74c5e5b..782a779fa8f 100755 --- a/src/ui/qgsvectorlayerpropertiesbase.ui +++ b/src/ui/qgsvectorlayerpropertiesbase.ui @@ -46,16 +46,7 @@ QFrame::Raised - - 0 - - - 0 - - - 0 - - + 0 @@ -282,16 +273,7 @@ QFrame::Raised - - 0 - - - 0 - - - 0 - - + 0 @@ -303,20 +285,11 @@ - 13 + 5 - - 0 - - - 0 - - - 0 - - + 0 @@ -328,16 +301,7 @@ QFrame::Raised - - 0 - - - 0 - - - 0 - - + 0 @@ -354,16 +318,7 @@ - - 0 - - - 0 - - - 0 - - + 0 @@ -379,21 +334,12 @@ 0 0 - 338 - 401 + 651 + 537 - - 0 - - - 0 - - - 0 - - + 0 @@ -448,16 +394,7 @@ border-radius: 2px; QFrame::Raised - - 0 - - - 0 - - - 0 - - + 0 @@ -506,7 +443,7 @@ border-radius: 2px; false - + vectorgeneral @@ -521,7 +458,7 @@ border-radius: 2px; - + Qt::StrongFocus @@ -629,16 +566,7 @@ border-radius: 2px; - - 0 - - - 0 - - - 0 - - + 0 @@ -654,21 +582,12 @@ border-radius: 2px; 0 0 - 100 - 30 + 651 + 537 - - 0 - - - 0 - - - 0 - - + 0 @@ -698,16 +617,7 @@ border-radius: 2px; - - 0 - - - 0 - - - 0 - - + 0 @@ -730,16 +640,7 @@ border-radius: 2px; - - 0 - - - 0 - - - 0 - - + 0 @@ -755,21 +656,12 @@ border-radius: 2px; 0 0 - 100 - 30 + 651 + 537 - - 0 - - - 0 - - - 0 - - + 0 @@ -790,16 +682,7 @@ border-radius: 2px; - - 0 - - - 0 - - - 0 - - + 0 @@ -815,21 +698,12 @@ border-radius: 2px; 0 0 - 673 - 317 + 651 + 537 - - 0 - - - 0 - - - 0 - - + 0 @@ -842,7 +716,7 @@ border-radius: 2px; - + Qt::StrongFocus @@ -1021,6 +895,70 @@ border-radius: 2px; + + + + + + <html><head/><body><p>Some data providers can notify QGIS (e.g. PostgreSQL) with a message. If this is the case for this layer's data provider, notification will refresh the layer. </p></body></html> + + + Refresh layer on notification + + + true + + + + + + + true + + + QFrame::NoFrame + + + QFrame::Plain + + + + 0 + + + 0 + + + 0 + + + + + true + + + <html><head/><body><p>Check if only a specific message must refresh the layer (i.e. not all data source notifications)</p></body></html> + + + Only if message is + + + + + + + false + + + <html><head/><body><p>Notification message that will refresh the layer.</p></body></html> + + + + + + + + @@ -1042,16 +980,7 @@ border-radius: 2px; - - 0 - - - 0 - - - 0 - - + 0 @@ -1060,16 +989,7 @@ border-radius: 2px; true - - 0 - - - 0 - - - 0 - - + 0 @@ -1080,7 +1000,7 @@ border-radius: 2px; - + Qt::StrongFocus @@ -1131,7 +1051,7 @@ border-radius: 2px; - + Qt::StrongFocus @@ -1150,16 +1070,7 @@ border-radius: 2px; - - 0 - - - 0 - - - 0 - - + 0 @@ -1180,16 +1091,7 @@ border-radius: 2px; - - 0 - - - 0 - - - 0 - - + 0 @@ -1216,16 +1118,7 @@ border-radius: 2px; - - 0 - - - 0 - - - 0 - - + 0 @@ -1241,21 +1134,12 @@ border-radius: 2px; 0 0 - 199 - 123 + 128 + 116 - - 0 - - - 0 - - - 0 - - + 0 @@ -1339,16 +1223,7 @@ border-radius: 2px; - - 0 - - - 0 - - - 0 - - + 0 @@ -1371,16 +1246,7 @@ border-radius: 2px; - - 0 - - - 0 - - - 0 - - + 0 @@ -1403,16 +1269,7 @@ border-radius: 2px; - - 0 - - - 0 - - - 0 - - + 0 @@ -1431,16 +1288,7 @@ border-radius: 2px; - - 0 - - - 0 - - - 0 - - + 0 @@ -1496,16 +1344,7 @@ border-radius: 2px; - - 0 - - - 0 - - - 0 - - + 0 @@ -1521,21 +1360,12 @@ border-radius: 2px; 0 0 - 629 - 527 + 651 + 537 - - 0 - - - 0 - - - 0 - - + 0 @@ -1543,7 +1373,7 @@ border-radius: 2px; Description - + vectormeta @@ -1685,7 +1515,7 @@ border-radius: 2px; Attribution - + vectormeta @@ -1731,7 +1561,7 @@ border-radius: 2px; MetadataUrl - + vectormeta @@ -1929,16 +1759,7 @@ border-radius: 2px; QFrame::Raised - - 0 - - - 0 - - - 0 - - + 0 @@ -1964,9 +1785,9 @@ border-radius: 2px; 1 - QgsProjectionSelectionWidget + QgsFieldExpressionWidget QWidget -
qgsprojectionselectionwidget.h
+
qgsfieldexpressionwidget.h
1
@@ -1974,6 +1795,12 @@ border-radius: 2px; QLineEdit
qgsfilterlineedit.h
+ + QgsProjectionSelectionWidget + QWidget +
qgsprojectionselectionwidget.h
+ 1 +
QgsScaleRangeWidget QWidget @@ -1991,12 +1818,6 @@ border-radius: 2px;
qgslayertreeembeddedconfigwidget.h
1
- - QgsFieldExpressionWidget - QWidget -
qgsfieldexpressionwidget.h
- 1 -
QgsLayerTreeView QTreeView @@ -2090,5 +1911,37 @@ border-radius: 2px; + + mRefreshLayerNotificationCheckBox + toggled(bool) + mNotificationFrame + setEnabled(bool) + + + 260 + 363 + + + 587 + 364 + + + + + mNotificationMessageCheckBox + toggled(bool) + mNotifyMessagValueLineEdit + setEnabled(bool) + + + 426 + 363 + + + 652 + 364 + + + diff --git a/tests/src/python/test_provider_postgres.py b/tests/src/python/test_provider_postgres.py index 3cf983519f2..2c4461d9262 100644 --- a/tests/src/python/test_provider_postgres.py +++ b/tests/src/python/test_provider_postgres.py @@ -17,6 +17,7 @@ import qgis # NOQA import psycopg2 import os +import time from qgis.core import ( QgsVectorLayer, @@ -33,13 +34,13 @@ from qgis.core import ( QgsRectangle ) from qgis.gui import QgsGui -from qgis.PyQt.QtCore import QDate, QTime, QDateTime, QVariant, QDir +from qgis.PyQt.QtCore import QDate, QTime, QDateTime, QVariant, QDir, QObject from qgis.testing import start_app, unittest from qgis.PyQt.QtXml import QDomDocument from utilities import unitTestDataPath from providertestbase import ProviderTestCase -start_app() +QGISAPP = start_app() TEST_DATA_DIR = unitTestDataPath() @@ -809,6 +810,42 @@ class TestPyQgsPostgresProvider(unittest.TestCase, ProviderTestCase): self.assertEqual(vl2.extent(), originalExtent) + def testNotify(self): + vl0 = QgsVectorLayer(self.dbconn + ' sslmode=disable key=\'pk\' srid=4326 type=POLYGON table="qgis_test"."some_poly_data" (geom) sql=', 'test', 'postgres') + vl0.dataProvider().setListening(True) + + class Notified(QObject): + + def __init__(self): + super(Notified, self).__init__() + self.received = "" + + def receive(self, msg): + self.received = msg + + notified = Notified() + vl0.dataProvider().notify.connect(notified.receive) + + vl0.dataProvider().setListening(True) + + cur = self.con.cursor() + ok = False + start = time.time() + while True: + cur.execute("NOTIFY qgis, 'my message'") + self.con.commit() + QGISAPP.processEvents() + if notified.received == "my message": + ok = True + break + if (time.time() - start) > 5: # timeout + break + + vl0.dataProvider().notify.disconnect(notified.receive) + vl0.dataProvider().setListening(False) + + self.assertTrue(ok) + class TestPyQgsPostgresProviderCompoundKey(unittest.TestCase, ProviderTestCase):