Skip to content

Commit 02e3916

Browse files
committedSep 22, 2017
[FEATURE] layer refresh and trigger actions on provider notification
[needs-docs] In vector layer properties (only usefull for postgres datasources) **in the rendering tab** A "Refresh layer on notification" checkbox has been added to refresh layer on provider notification. For a postgres datasource, if a `NOTIFY qgis;` command is issued by one of the database clients, a refresh of the layer will occur. If the "Only if message is" checkbox is checked, the notification will trigger the refresh only if the message contend is the one specified, e.g. if the user enters "refresh" in the box right next to the "Only if message is" checkbox, then a `NOTIFY qgis, 'refresh';` command in the datatabase will trigger a layer refresh, but `NOTIFY qgis;` or `NOTIFY qgis, 'something else';` won't. **in the actions tab** A column "On notification" has been added, the action editor widget a has text field "Execute if notification message matches" to specify a filter for notification from the provider. The filter is a Perl-type regex. Note that, as opposed to the "layer refresh" that Exemple: - QGIS side "Execute if notification message matches" `^trigger my action` - Postgres side: `NOTIFY qgis, 'trigger my action'` will trigger the action - Postgres side: `NOTIFY qgis, 'trigger my action some additional data'` will trigger the action - Postgres side: `NOTIFY qgis, 'do not trigger my action some additional data'` will NOT trigger the action Please note that if the `^`, which means "starts with", in `^trigger my action` had been ommited, the last notification would have triggered the action because the notification message contains the `trigger my action` A new qgis variable `notification_message` is available for use in actions, it holds the contend of the notification message. To continue with the previous exemple, if the action is of python type with the code: ```python print('[% @notification_message %]') ``` The three notifictions above will result in two printed lines ``` trigger my action trigger my action some additional data ``` User Warning: For postgres providers, if the "Refresh layer on notification" is checked, or if one layer action has "On notification" specified, a new connection to the database is made to listen to postgres notifications. This olds even if transaction groups are enabled at the project level. Note that once the notification mechanism is started in a QGIS session, it will not stop, even if there is no more need for it (Refresh layer on notification" unchecked and no "On notification" in any action). Consequently the connection listening to notification will remain open. IMPLEMENTATION DETAILS: A notify signal has been added to the abstract QgsVectorDataProvider along with a setListening function that enables/disble the notification mechanism. For the moment only the postgres provider implements the notification. QgsAction has a notificationMessage member function that holds the regex to match to trigger action QgsActionManager becomes a QObject and is doing the filtering and execute actions on notifications. The notification notion extends beyond SRGBD servers (postgres and oracle at least have the notify) and the "watch file" in the delimitedtext provider could also benefit from this interface. For the postgres provider a thread is created with a second connection to the database. This thread is responsible for listening postgres notifications. It would be nice to avoid the creation of one listening chanel per provider in the case transaction groups are enabled. Please note that when listening starts (a thread and connection is created in the postgres provider) it cannot be stopped by removing the connected actions or unchecking the refresh check box. Indeed, since we don't know who needs the signals, we dont't want to stop the service. The service will not restart in the next qgis session though. If this behavior is not deemed appropriate, we could use ``` int QObject::receivers ( const char * signal ) const ``` and have QgsDataProvider::setListening return a bool to tell the caller if the signal has actually been closed.
1 parent d6d7c6e commit 02e3916

32 files changed

+879
-450
lines changed
 

‎python/core/qgsaction.sip

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class QgsAction
4444
\param capture If this is set to true, the output will be captured when an action is run
4545
%End
4646

47-
QgsAction( ActionType type, const QString &description, const QString &action, const QString &icon, bool capture, const QString &shortTitle = QString(), const QSet<QString> &actionScopes = QSet<QString>() );
47+
QgsAction( ActionType type, const QString &description, const QString &action, const QString &icon, bool capture, const QString &shortTitle = QString(), const QSet<QString> &actionScopes = QSet<QString>(), const QString &notificationMessage = QString() );
4848
%Docstring
4949
Create a new QgsAction
5050

@@ -55,6 +55,7 @@ class QgsAction
5555
\param capture If this is set to true, the output will be captured when an action is run
5656
\param shortTitle A short string used to label user interface elements like buttons
5757
\param actionScopes A set of scopes in which this action will be available
58+
\param notificationMessage A particular message which reception will trigger the action
5859
%End
5960

6061
QString name() const;
@@ -103,6 +104,14 @@ The icon
103104
How the content is interpreted depends on the type() and
104105
the actionScope().
105106

107+
.. versionadded:: 3.0
108+
:rtype: str
109+
%End
110+
111+
QString notificationMessage() const;
112+
%Docstring
113+
Returns the notification message that triggers the action
114+
106115
.. versionadded:: 3.0
107116
:rtype: str
108117
%End

‎python/core/qgsactionmanager.sip

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313

1414

15-
class QgsActionManager
15+
class QgsActionManager: QObject
1616
{
1717
%Docstring
1818
Storage and management of actions associated with a layer.

‎python/core/qgsdataprovider.sip

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,18 @@ Current time stamp of data source
361361
:rtype: QVariant
362362
%End
363363

364+
virtual void setListening( bool isListening );
365+
%Docstring
366+
Set whether the provider will listen to datasource notifications
367+
If set, the provider will issue notify signals.
368+
369+
The default implementation does nothing.
370+
371+
.. seealso:: notify
372+
373+
.. versionadded:: 3.0
374+
%End
375+
364376
signals:
365377

366378
void fullExtentCalculated();
@@ -379,6 +391,16 @@ Current time stamp of data source
379391
feature ids should be invalidated.
380392
%End
381393

394+
void notify( const QString &msg ) const;
395+
%Docstring
396+
Emitted when datasource issues a notification
397+
398+
.. seealso:: setListening
399+
400+
.. versionadded:: 3.0
401+
%End
402+
403+
382404
protected:
383405

384406

‎python/core/qgsexpressioncontext.sip

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,13 @@ class QgsExpressionContextUtils
940940
:rtype: QgsExpressionContextScope
941941
%End
942942

943+
static QgsExpressionContextScope *notificationScope( const QString &message = QString() ) /Factory/;
944+
%Docstring
945+
Creates a new scope which contains variables and functions relating to provider notifications
946+
\param message the notification message
947+
:rtype: QgsExpressionContextScope
948+
%End
949+
943950
static void registerContextFunctions();
944951
%Docstring
945952
Registers all known core functions provided by QgsExpressionContextScope objects.

‎python/core/qgsmaplayer.sip

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,39 @@ Time stamp of data source in the moment when data/metadata were loaded by provid
953953
:rtype: bool
954954
%End
955955

956+
void setRefreshOnNotifyEnabled( bool enabled );
957+
%Docstring
958+
Set whether provider notification is connected to triggerRepaint
959+
960+
.. versionadded:: 3.0
961+
%End
962+
963+
void setRefreshOnNofifyMessage( const QString &message );
964+
%Docstring
965+
Set the notification message that triggers repaine
966+
If refresh on notification is enabled, the notification will triggerRepaint only
967+
if the notification message is equal to \param message
968+
969+
.. versionadded:: 3.0
970+
%End
971+
972+
QString refreshOnNotifyMessage() const;
973+
%Docstring
974+
Returns the message that should be notified by the provider to triggerRepaint
975+
976+
.. versionadded:: 3.0
977+
:rtype: str
978+
%End
979+
980+
bool isRefreshOnNotifyEnabled() const;
981+
%Docstring
982+
Returns true if the refresh on provider nofification is enabled
983+
984+
.. versionadded:: 3.0
985+
:rtype: bool
986+
%End
987+
988+
956989
signals:
957990

958991
void statusChanged( const QString &status );
@@ -1134,6 +1167,7 @@ Checks whether a new set of dependencies will introduce a cycle
11341167
:rtype: bool
11351168
%End
11361169

1170+
11371171
};
11381172

11391173

‎src/app/qgsattributeactiondialog.cpp

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,16 @@ void QgsAttributeActionDialog::insertRow( int row, const QgsAction &action )
145145
headerItem->setData( Qt::UserRole, action.iconPath() );
146146
mAttributeActionTable->setVerticalHeaderItem( row, headerItem );
147147

148+
// Notification message
149+
mAttributeActionTable->setItem( row, NotificationMessage, new QTableWidgetItem( action.notificationMessage() ) );
150+
148151
updateButtons();
149152
}
150153

151-
void QgsAttributeActionDialog::insertRow( int row, QgsAction::ActionType type, const QString &name, const QString &actionText, const QString &iconPath, bool capture, const QString &shortTitle, const QSet<QString> &actionScopes )
154+
void QgsAttributeActionDialog::insertRow( int row, QgsAction::ActionType type, const QString &name, const QString &actionText, const QString &iconPath, bool capture, const QString &shortTitle, const QSet<QString> &actionScopes, const QString &notificationMessage )
152155
{
153156
if ( uniqueName( name ) == name )
154-
insertRow( row, QgsAction( type, name, actionText, iconPath, capture, shortTitle, actionScopes ) );
157+
insertRow( row, QgsAction( type, name, actionText, iconPath, capture, shortTitle, actionScopes, notificationMessage ) );
155158
}
156159

157160
void QgsAttributeActionDialog::moveUp()
@@ -219,7 +222,9 @@ QgsAction QgsAttributeActionDialog::rowToAction( int row ) const
219222
mAttributeActionTable->verticalHeaderItem( row )->data( Qt::UserRole ).toString(),
220223
mAttributeActionTable->item( row, Capture )->checkState() == Qt::Checked,
221224
mAttributeActionTable->item( row, ShortTitle )->text(),
222-
mAttributeActionTable->item( row, ActionScopes )->data( Qt::UserRole ).value<QSet<QString>>() );
225+
mAttributeActionTable->item( row, ActionScopes )->data( Qt::UserRole ).value<QSet<QString>>(),
226+
mAttributeActionTable->item( row, NotificationMessage )->text()
227+
);
223228
return action;
224229
}
225230

@@ -273,7 +278,7 @@ void QgsAttributeActionDialog::insert()
273278
{
274279
QString name = uniqueName( dlg.description() );
275280

276-
insertRow( pos, dlg.type(), name, dlg.actionText(), dlg.iconPath(), dlg.capture(), dlg.shortTitle(), dlg.actionScopes() );
281+
insertRow( pos, dlg.type(), name, dlg.actionText(), dlg.iconPath(), dlg.capture(), dlg.shortTitle(), dlg.actionScopes(), dlg.notificationMessage() );
277282
}
278283
}
279284

@@ -300,14 +305,14 @@ void QgsAttributeActionDialog::updateButtons()
300305
void QgsAttributeActionDialog::addDefaultActions()
301306
{
302307
int pos = 0;
303-
insertRow( pos++, QgsAction::Generic, tr( "Echo attribute's value" ), QStringLiteral( "echo \"[% \"MY_FIELD\" %]\"" ), QLatin1String( "" ), true, tr( "Attribute Value" ), QSet<QString>() << QStringLiteral( "Field" ) );
304-
insertRow( pos++, QgsAction::Generic, tr( "Run an application" ), QStringLiteral( "ogr2ogr -f \"ESRI Shapefile\" \"[% \"OUTPUT_PATH\" %]\" \"[% \"INPUT_FILE\" %]\"" ), QLatin1String( "" ), true, tr( "Run application" ), QSet<QString>() << QStringLiteral( "Feature" ) << QStringLiteral( "Canvas" ) );
305-
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<QString>() << QStringLiteral( "Feature" ) << QStringLiteral( "Canvas" ) );
306-
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<QString>() << QStringLiteral( "Field" ) );
307-
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<QString>() << QStringLiteral( "Canvas" ) );
308-
insertRow( pos++, QgsAction::OpenUrl, tr( "Open file" ), QStringLiteral( "[% \"PATH\" %]" ), QLatin1String( "" ), false, tr( "Open file" ), QSet<QString>() << QStringLiteral( "Feature" ) << QStringLiteral( "Canvas" ) );
309-
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<QString>() << QStringLiteral( "Field" ) );
310-
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<QString>() << QStringLiteral( "Layer" ) );
308+
insertRow( pos++, QgsAction::Generic, tr( "Echo attribute's value" ), QStringLiteral( "echo \"[% \"MY_FIELD\" %]\"" ), QLatin1String( "" ), true, tr( "Attribute Value" ), QSet<QString>() << QStringLiteral( "Field" ), QString() );
309+
insertRow( pos++, QgsAction::Generic, tr( "Run an application" ), QStringLiteral( "ogr2ogr -f \"ESRI Shapefile\" \"[% \"OUTPUT_PATH\" %]\" \"[% \"INPUT_FILE\" %]\"" ), QLatin1String( "" ), true, tr( "Run application" ), QSet<QString>() << QStringLiteral( "Feature" ) << QStringLiteral( "Canvas" ), QString() );
310+
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<QString>() << QStringLiteral( "Feature" ) << QStringLiteral( "Canvas" ), QString() );
311+
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<QString>() << QStringLiteral( "Field" ), QString() );
312+
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<QString>() << QStringLiteral( "Canvas" ), QString() );
313+
insertRow( pos++, QgsAction::OpenUrl, tr( "Open file" ), QStringLiteral( "[% \"PATH\" %]" ), QLatin1String( "" ), false, tr( "Open file" ), QSet<QString>() << QStringLiteral( "Feature" ) << QStringLiteral( "Canvas" ), QString() );
314+
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<QString>() << QStringLiteral( "Field" ), QString() );
315+
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<QString>() << QStringLiteral( "Layer" ), QString() );
311316
}
312317

313318
void QgsAttributeActionDialog::itemDoubleClicked( QTableWidgetItem *item )
@@ -322,6 +327,7 @@ void QgsAttributeActionDialog::itemDoubleClicked( QTableWidgetItem *item )
322327
mAttributeActionTable->item( row, ActionText )->text(),
323328
mAttributeActionTable->item( row, Capture )->checkState() == Qt::Checked,
324329
mAttributeActionTable->item( row, ActionScopes )->data( Qt::UserRole ).value<QSet<QString>>(),
330+
mAttributeActionTable->item( row, NotificationMessage )->text(),
325331
mLayer
326332
);
327333

@@ -335,6 +341,7 @@ void QgsAttributeActionDialog::itemDoubleClicked( QTableWidgetItem *item )
335341
mAttributeActionTable->item( row, ShortTitle )->setText( actionProperties.shortTitle() );
336342
mAttributeActionTable->item( row, ActionText )->setText( actionProperties.actionText() );
337343
mAttributeActionTable->item( row, Capture )->setCheckState( actionProperties.capture() ? Qt::Checked : Qt::Unchecked );
344+
mAttributeActionTable->item( row, NotificationMessage )->setText( actionProperties.notificationMessage() );
338345

339346
QTableWidgetItem *item = mAttributeActionTable->item( row, ActionScopes );
340347
QStringList actionScopes = actionProperties.actionScopes().toList();

‎src/app/qgsattributeactiondialog.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ class APP_EXPORT QgsAttributeActionDialog: public QWidget, private Ui::QgsAttrib
4343
ShortTitle,
4444
ActionText,
4545
Capture,
46-
ActionScopes
46+
ActionScopes,
47+
NotificationMessage
4748
};
4849

4950
public:
@@ -69,7 +70,7 @@ class APP_EXPORT QgsAttributeActionDialog: public QWidget, private Ui::QgsAttrib
6970

7071
private:
7172
void insertRow( int row, const QgsAction &action );
72-
void insertRow( int row, QgsAction::ActionType type, const QString &name, const QString &actionText, const QString &iconPath, bool capture, const QString &shortTitle, const QSet<QString> &actionScopes );
73+
void insertRow( int row, QgsAction::ActionType type, const QString &name, const QString &actionText, const QString &iconPath, bool capture, const QString &shortTitle, const QSet<QString> &actionScopes, const QString &notificationMessage );
7374
void swapRows( int row1, int row2 );
7475
QgsAction rowToAction( int row ) const;
7576

‎src/app/qgsattributeactionpropertiesdialog.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
#include <QFileDialog>
3232
#include <QImageWriter>
3333

34-
QgsAttributeActionPropertiesDialog::QgsAttributeActionPropertiesDialog( QgsAction::ActionType type, const QString &description, const QString &shortTitle, const QString &iconPath, const QString &actionText, bool capture, const QSet<QString> &actionScopes, QgsVectorLayer *layer, QWidget *parent )
34+
QgsAttributeActionPropertiesDialog::QgsAttributeActionPropertiesDialog( QgsAction::ActionType type, const QString &description, const QString &shortTitle, const QString &iconPath, const QString &actionText, bool capture, const QSet<QString> &actionScopes, const QString &notificationMessage, QgsVectorLayer *layer, QWidget *parent )
3535
: QDialog( parent )
3636
, mLayer( layer )
3737
{
@@ -44,6 +44,7 @@ QgsAttributeActionPropertiesDialog::QgsAttributeActionPropertiesDialog( QgsActio
4444
mIconPreview->setPixmap( QPixmap( iconPath ) );
4545
mActionText->setText( actionText );
4646
mCaptureOutput->setChecked( capture );
47+
mNotificationMessage->setText( notificationMessage );
4748

4849
init( actionScopes );
4950
}
@@ -101,6 +102,12 @@ QSet<QString> QgsAttributeActionPropertiesDialog::actionScopes() const
101102
return actionScopes;
102103
}
103104

105+
QString QgsAttributeActionPropertiesDialog::notificationMessage() const
106+
{
107+
return mNotificationMessage->text();
108+
}
109+
110+
104111
bool QgsAttributeActionPropertiesDialog::capture() const
105112
{
106113
return mCaptureOutput->isChecked();
@@ -119,6 +126,8 @@ QgsExpressionContext QgsAttributeActionPropertiesDialog::createExpressionContext
119126
}
120127
}
121128

129+
context << QgsExpressionContextUtils::notificationScope();
130+
122131
return context;
123132
}
124133

‎src/app/qgsattributeactionpropertiesdialog.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class QgsAttributeActionPropertiesDialog: public QDialog, private Ui::QgsAttribu
2828
Q_OBJECT
2929

3030
public:
31-
QgsAttributeActionPropertiesDialog( QgsAction::ActionType type, const QString &description, const QString &shortTitle, const QString &iconPath, const QString &actionText, bool capture, const QSet<QString> &actionScopes, QgsVectorLayer *layer, QWidget *parent = nullptr );
31+
QgsAttributeActionPropertiesDialog( QgsAction::ActionType type, const QString &description, const QString &shortTitle, const QString &iconPath, const QString &actionText, bool capture, const QSet<QString> &actionScopes, const QString &notificationMessage, QgsVectorLayer *layer, QWidget *parent = nullptr );
3232

3333
QgsAttributeActionPropertiesDialog( QgsVectorLayer *layer, QWidget *parent = nullptr );
3434

@@ -44,6 +44,8 @@ class QgsAttributeActionPropertiesDialog: public QDialog, private Ui::QgsAttribu
4444

4545
QSet<QString> actionScopes() const;
4646

47+
QString notificationMessage() const;
48+
4749
bool capture() const;
4850

4951
virtual QgsExpressionContext createExpressionContext() const override;

‎src/app/qgsvectorlayerproperties.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,11 @@ void QgsVectorLayerProperties::syncToLayer()
454454
mRefreshLayerIntervalSpinBox->setEnabled( mLayer->hasAutoRefreshEnabled() );
455455
mRefreshLayerIntervalSpinBox->setValue( mLayer->autoRefreshInterval() / 1000.0 );
456456

457+
mRefreshLayerNotificationCheckBox->setChecked( mLayer->isRefreshOnNotifyEnabled() );
458+
mNotificationMessageCheckBox->setChecked( !mLayer->refreshOnNotifyMessage().isEmpty() );
459+
mNotifyMessagValueLineEdit->setText( mLayer->refreshOnNotifyMessage() );
460+
461+
457462
// load appropriate symbology page (V1 or V2)
458463
updateSymbologyPage();
459464

@@ -632,6 +637,9 @@ void QgsVectorLayerProperties::apply()
632637
mLayer->setAutoRefreshInterval( mRefreshLayerIntervalSpinBox->value() * 1000.0 );
633638
mLayer->setAutoRefreshEnabled( mRefreshLayerCheckBox->isChecked() );
634639

640+
mLayer->setRefreshOnNotifyEnabled( mRefreshLayerNotificationCheckBox->isChecked() );
641+
mLayer->setRefreshOnNofifyMessage( mNotificationMessageCheckBox->isChecked() ? mNotifyMessagValueLineEdit->text() : QString() );
642+
635643
mOldJoins = mLayer->vectorJoins();
636644

637645
//save variables

‎src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ ENDIF(NOT MSVC)
562562

563563
SET(QGIS_CORE_MOC_HDRS
564564
qgsapplication.h
565+
qgsactionmanager.h
565566
qgsactionscoperegistry.h
566567
qgsanimatedicon.h
567568
qgsbrowsermodel.h

‎src/core/expression/qgsexpression.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,9 @@ void QgsExpression::initVariableHelp()
703703

704704
//processing variables
705705
sVariableHelpTexts.insert( QStringLiteral( "algorithm_id" ), QCoreApplication::translate( "algorithm_id", "Unique ID for algorithm." ) );
706+
707+
//provider notification
708+
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)." ) );
706709
}
707710

708711
QString QgsExpression::variableHelpText( const QString &variableName )

‎src/core/qgsaction.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ void QgsAction::readXml( const QDomNode &actionNode )
119119
mIcon = actionElement.attributeNode( QStringLiteral( "icon" ) ).value();
120120
mCaptureOutput = actionElement.attributeNode( QStringLiteral( "capture" ) ).value().toInt() != 0;
121121
mShortTitle = actionElement.attributeNode( QStringLiteral( "shortTitle" ) ).value();
122+
mNotificationMessage = actionElement.attributeNode( QStringLiteral( "notificationMessage" ) ).value();
122123
mId = QUuid( actionElement.attributeNode( QStringLiteral( "id" ) ).value() );
123124
if ( mId.isNull() )
124125
mId = QUuid::createUuid();
@@ -133,6 +134,7 @@ void QgsAction::writeXml( QDomNode &actionsNode ) const
133134
actionSetting.setAttribute( QStringLiteral( "icon" ), mIcon );
134135
actionSetting.setAttribute( QStringLiteral( "action" ), mCommand );
135136
actionSetting.setAttribute( QStringLiteral( "capture" ), mCaptureOutput );
137+
actionSetting.setAttribute( QStringLiteral( "notificationMessage" ), mNotificationMessage );
136138
actionSetting.setAttribute( QStringLiteral( "id" ), mId.toString() );
137139

138140
Q_FOREACH ( const QString &scope, mActionScopes )

‎src/core/qgsaction.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,17 @@ class CORE_EXPORT QgsAction
7777
* \param capture If this is set to true, the output will be captured when an action is run
7878
* \param shortTitle A short string used to label user interface elements like buttons
7979
* \param actionScopes A set of scopes in which this action will be available
80+
* \param notificationMessage A particular message which reception will trigger the action
8081
*/
81-
QgsAction( ActionType type, const QString &description, const QString &action, const QString &icon, bool capture, const QString &shortTitle = QString(), const QSet<QString> &actionScopes = QSet<QString>() )
82+
QgsAction( ActionType type, const QString &description, const QString &action, const QString &icon, bool capture, const QString &shortTitle = QString(), const QSet<QString> &actionScopes = QSet<QString>(), const QString &notificationMessage = QString() )
8283
: mType( type )
8384
, mDescription( description )
8485
, mShortTitle( shortTitle )
8586
, mIcon( icon )
8687
, mCommand( action )
8788
, mCaptureOutput( capture )
8889
, mActionScopes( actionScopes )
90+
, mNotificationMessage( notificationMessage )
8991
, mId( QUuid::createUuid() )
9092
{}
9193

@@ -124,6 +126,13 @@ class CORE_EXPORT QgsAction
124126
*/
125127
QString command() const { return mCommand; }
126128

129+
/**
130+
* Returns the notification message that triggers the action
131+
*
132+
* \since QGIS 3.0
133+
*/
134+
QString notificationMessage() const { return mNotificationMessage; }
135+
127136
//! The action type
128137
ActionType type() const { return mType; }
129138

@@ -190,6 +199,7 @@ class CORE_EXPORT QgsAction
190199
QString mCommand;
191200
bool mCaptureOutput = false;
192201
QSet<QString> mActionScopes;
202+
QString mNotificationMessage;
193203
mutable std::shared_ptr<QAction> mAction;
194204
QUuid mId;
195205
};

‎src/core/qgsactionmanager.cpp

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "qgsproject.h"
3030
#include "qgslogger.h"
3131
#include "qgsexpression.h"
32+
#include "qgsdataprovider.h"
3233

3334
#include <QList>
3435
#include <QStringList>
@@ -38,6 +39,7 @@
3839
#include <QUrl>
3940
#include <QDir>
4041
#include <QFileInfo>
42+
#include <QRegularExpression>
4143

4244

4345
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
5658

5759
void QgsActionManager::addAction( const QgsAction &action )
5860
{
61+
QgsDebugMsg( "add action " + action.name() );
5962
mActions.append( action );
63+
if ( mLayer && mLayer->dataProvider() && !action.notificationMessage().isEmpty() )
64+
{
65+
mLayer->dataProvider()->setListening( true );
66+
if ( !mOnNotifyConnected )
67+
{
68+
QgsDebugMsg( "connecting to notify" );
69+
connect( mLayer->dataProvider(), &QgsDataProvider::notify, this, &QgsActionManager::onNotifyRunActions );
70+
mOnNotifyConnected = true;
71+
}
72+
}
73+
}
74+
75+
void QgsActionManager::onNotifyRunActions( const QString &message )
76+
{
77+
for ( const QgsAction &act : qgsAsConst( mActions ) )
78+
{
79+
if ( !act.notificationMessage().isEmpty() && QRegularExpression( act.notificationMessage() ).match( message ).hasMatch() )
80+
{
81+
if ( !act.isValid() || !act.runable() )
82+
continue;
83+
84+
QgsExpressionContext context = createExpressionContext();
85+
86+
Q_ASSERT( mLayer ); // if there is no layer, then where is the notification coming from ?
87+
context << QgsExpressionContextUtils::layerScope( mLayer );
88+
context << QgsExpressionContextUtils::notificationScope( message );
89+
90+
QString expandedAction = QgsExpression::replaceExpressionText( act.command(), &context );
91+
if ( expandedAction.isEmpty() )
92+
continue;
93+
runAction( QgsAction( act.type(), act.name(), expandedAction, act.capture() ) );
94+
}
95+
}
6096
}
6197

6298
void QgsActionManager::removeAction( const QUuid &actionId )
6399
{
64100
int i = 0;
65-
Q_FOREACH ( const QgsAction &action, mActions )
101+
for ( const QgsAction &action : qgsAsConst( mActions ) )
66102
{
67103
if ( action.id() == actionId )
68104
{
69105
mActions.removeAt( i );
70-
return;
106+
break;
71107
}
72108
++i;
73109
}
110+
111+
if ( mOnNotifyConnected )
112+
{
113+
bool hasActionOnNotify = false;
114+
for ( const QgsAction &action : qgsAsConst( mActions ) )
115+
hasActionOnNotify |= !action.notificationMessage().isEmpty();
116+
if ( !hasActionOnNotify && mLayer && mLayer->dataProvider() )
117+
{
118+
// note that there is no way of knowing if the provider is listening only because
119+
// this class has hasked it to, so we do not reset the provider listening state here
120+
disconnect( mLayer->dataProvider(), &QgsDataProvider::notify, this, &QgsActionManager::onNotifyRunActions );
121+
mOnNotifyConnected = false;
122+
}
123+
}
74124
}
75125

76126
void QgsActionManager::doAction( const QUuid &actionId, const QgsFeature &feature, int defaultValueIndex )
@@ -109,6 +159,13 @@ void QgsActionManager::doAction( const QUuid &actionId, const QgsFeature &feat,
109159
void QgsActionManager::clearActions()
110160
{
111161
mActions.clear();
162+
if ( mOnNotifyConnected && mLayer && mLayer->dataProvider() )
163+
{
164+
// note that there is no way of knowing if the provider is listening only because
165+
// this class has hasked it to, so we do not reset the provider listening state here
166+
disconnect( mLayer->dataProvider(), &QgsDataProvider::notify, this, &QgsActionManager::onNotifyRunActions );
167+
mOnNotifyConnected = false;
168+
}
112169
}
113170

114171
QList<QgsAction> QgsActionManager::actions( const QString &actionScope ) const
@@ -119,7 +176,7 @@ QList<QgsAction> QgsActionManager::actions( const QString &actionScope ) const
119176
{
120177
QList<QgsAction> actions;
121178

122-
Q_FOREACH ( const QgsAction &action, mActions )
179+
for ( const QgsAction &action : qgsAsConst( mActions ) )
123180
{
124181
if ( action.actionScopes().contains( actionScope ) )
125182
actions.append( action );
@@ -174,7 +231,7 @@ bool QgsActionManager::writeXml( QDomNode &layer_node ) const
174231
aActions.appendChild( defaultActionElement );
175232
}
176233

177-
Q_FOREACH ( const QgsAction &action, mActions )
234+
for ( const QgsAction &action : qgsAsConst( mActions ) )
178235
{
179236
action.writeXml( aActions );
180237
}
@@ -185,7 +242,7 @@ bool QgsActionManager::writeXml( QDomNode &layer_node ) const
185242

186243
bool QgsActionManager::readXml( const QDomNode &layer_node )
187244
{
188-
mActions.clear();
245+
clearActions();
189246

190247
QDomNode aaNode = layer_node.namedItem( QStringLiteral( "attributeactions" ) );
191248

@@ -196,7 +253,7 @@ bool QgsActionManager::readXml( const QDomNode &layer_node )
196253
{
197254
QgsAction action;
198255
action.readXml( actionsettings.item( i ) );
199-
mActions.append( action );
256+
addAction( action );
200257
}
201258

202259
QDomNodeList defaultActionNodes = aaNode.toElement().elementsByTagName( "defaultAction" );
@@ -212,7 +269,7 @@ bool QgsActionManager::readXml( const QDomNode &layer_node )
212269

213270
QgsAction QgsActionManager::action( const QUuid &id )
214271
{
215-
Q_FOREACH ( const QgsAction &action, mActions )
272+
for ( const QgsAction &action : qgsAsConst( mActions ) )
216273
{
217274
if ( action.id() == id )
218275
return action;

‎src/core/qgsactionmanager.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "qgis_core.h"
2828
#include <QString>
2929
#include <QIcon>
30+
#include <QObject>
3031

3132
#include "qgsaction.h"
3233
#include "qgsfeature.h"
@@ -46,8 +47,10 @@ class QgsExpressionContext;
4647
* based on attributes of a given feature.
4748
*/
4849

49-
class CORE_EXPORT QgsActionManager
50+
class CORE_EXPORT QgsActionManager: public QObject
5051
{
52+
Q_OBJECT
53+
5154
public:
5255
//! Constructor
5356
QgsActionManager( QgsVectorLayer *layer )
@@ -148,7 +151,12 @@ class CORE_EXPORT QgsActionManager
148151

149152
QMap<QString, QUuid> mDefaultActions;
150153

154+
bool mOnNotifyConnected = false;
155+
151156
QgsExpressionContext createExpressionContext() const;
157+
158+
private slots:
159+
void onNotifyRunActions( const QString &message );
152160
};
153161

154162
#endif

‎src/core/qgsdataprovider.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,8 @@ QVariant QgsDataProvider::providerProperty( int property, const QVariant &defaul
3636
return mProviderProperties.value( property, defaultValue );
3737
}
3838

39+
void QgsDataProvider::setListening( bool isListening )
40+
{
41+
Q_UNUSED( isListening );
42+
}
43+

‎src/core/qgsdataprovider.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,18 @@ class CORE_EXPORT QgsDataProvider : public QObject
429429
*/
430430
QVariant providerProperty( int property, const QVariant &defaultValue ) const; // SIP_SKIP
431431

432+
/**
433+
* Set whether the provider will listen to datasource notifications
434+
* If set, the provider will issue notify signals.
435+
*
436+
* The default implementation does nothing.
437+
*
438+
* \see notify
439+
*
440+
* \since QGIS 3.0
441+
*/
442+
virtual void setListening( bool isListening );
443+
432444
signals:
433445

434446
/**
@@ -447,6 +459,16 @@ class CORE_EXPORT QgsDataProvider : public QObject
447459
*/
448460
void dataChanged();
449461

462+
/**
463+
* Emitted when datasource issues a notification
464+
*
465+
* \see setListening
466+
*
467+
* \since QGIS 3.0
468+
*/
469+
void notify( const QString &msg ) const;
470+
471+
450472
protected:
451473

452474
/**

‎src/core/qgsexpressioncontext.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,6 +1230,13 @@ QgsExpressionContextScope *QgsExpressionContextUtils::processingAlgorithmScope(
12301230
return scope.release();
12311231
}
12321232

1233+
QgsExpressionContextScope *QgsExpressionContextUtils::notificationScope( const QString &message )
1234+
{
1235+
std::unique_ptr< QgsExpressionContextScope > scope( new QgsExpressionContextScope() );
1236+
scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "notification_message" ), message, true ) );
1237+
return scope.release();
1238+
}
1239+
12331240
void QgsExpressionContextUtils::registerContextFunctions()
12341241
{
12351242
QgsExpression::registerFunction( new GetNamedProjectColor( nullptr ) );

‎src/core/qgsexpressioncontext.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,12 @@ class CORE_EXPORT QgsExpressionContextUtils
861861
*/
862862
static QgsExpressionContextScope *processingAlgorithmScope( const QgsProcessingAlgorithm *algorithm, const QVariantMap &parameters, QgsProcessingContext &context ) SIP_FACTORY;
863863

864+
/**
865+
* Creates a new scope which contains variables and functions relating to provider notifications
866+
* \param message the notification message
867+
*/
868+
static QgsExpressionContextScope *notificationScope( const QString &message = QString() ) SIP_FACTORY;
869+
864870
/** Registers all known core functions provided by QgsExpressionContextScope objects.
865871
*/
866872
static void registerContextFunctions();

‎src/core/qgsmaplayer.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,9 @@ bool QgsMapLayer::readLayerXml( const QDomElement &layerElement, const QgsReadWr
472472

473473
setAutoRefreshInterval( layerElement.attribute( QStringLiteral( "autoRefreshTime" ), 0 ).toInt() );
474474
setAutoRefreshEnabled( layerElement.attribute( QStringLiteral( "autoRefreshEnabled" ), QStringLiteral( "0" ) ).toInt() );
475+
setRefreshOnNofifyMessage( layerElement.attribute( QStringLiteral( "refreshOnNotifyMessage" ), QString() ) );
476+
setRefreshOnNotifyEnabled( layerElement.attribute( QStringLiteral( "refreshOnNotifyEnabled" ), QStringLiteral( "0" ) ).toInt() );
477+
475478

476479
// set name
477480
mnl = layerElement.namedItem( QStringLiteral( "layername" ) );
@@ -577,6 +580,9 @@ bool QgsMapLayer::writeLayerXml( QDomElement &layerElement, QDomDocument &docume
577580

578581
layerElement.setAttribute( QStringLiteral( "autoRefreshTime" ), QString::number( mRefreshTimer.interval() ) );
579582
layerElement.setAttribute( QStringLiteral( "autoRefreshEnabled" ), mRefreshTimer.isActive() ? 1 : 0 );
583+
layerElement.setAttribute( QStringLiteral( "refreshOnNotifyEnabled" ), mIsRefreshOnNofifyEnabled ? 1 : 0 );
584+
layerElement.setAttribute( QStringLiteral( "refreshOnNotifyMessage" ), mRefreshOnNofifyMessage );
585+
580586

581587
// ID
582588
QDomElement layerId = document.createElement( QStringLiteral( "id" ) );
@@ -1775,3 +1781,28 @@ bool QgsMapLayer::setDependencies( const QSet<QgsMapLayerDependency> &oDeps )
17751781
emit dependenciesChanged();
17761782
return true;
17771783
}
1784+
1785+
void QgsMapLayer::setRefreshOnNotifyEnabled( bool enabled )
1786+
{
1787+
if ( !dataProvider() )
1788+
return;
1789+
1790+
if ( enabled && !isRefreshOnNotifyEnabled() )
1791+
{
1792+
dataProvider()->setListening( enabled );
1793+
connect( dataProvider(), &QgsVectorDataProvider::notify, this, &QgsMapLayer::onNotifiedTriggerRepaint );
1794+
}
1795+
else if ( !enabled && isRefreshOnNotifyEnabled() )
1796+
{
1797+
// we don't want to disable provider listening because someone else could need it (e.g. actions)
1798+
disconnect( dataProvider(), &QgsVectorDataProvider::notify, this, &QgsMapLayer::onNotifiedTriggerRepaint );
1799+
}
1800+
mIsRefreshOnNofifyEnabled = enabled;
1801+
}
1802+
1803+
void QgsMapLayer::onNotifiedTriggerRepaint( const QString &message )
1804+
{
1805+
if ( refreshOnNotifyMessage().isEmpty() || refreshOnNotifyMessage() == message )
1806+
triggerRepaint();
1807+
}
1808+

‎src/core/qgsmaplayer.h

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,37 @@ class CORE_EXPORT QgsMapLayer : public QObject
847847
*/
848848
virtual bool setDependencies( const QSet<QgsMapLayerDependency> &layers );
849849

850+
/**
851+
* Set whether provider notification is connected to triggerRepaint
852+
*
853+
* \since QGIS 3.0
854+
*/
855+
void setRefreshOnNotifyEnabled( bool enabled );
856+
857+
/**
858+
* Set the notification message that triggers repaine
859+
* If refresh on notification is enabled, the notification will triggerRepaint only
860+
* if the notification message is equal to \param message
861+
*
862+
* \since QGIS 3.0
863+
*/
864+
void setRefreshOnNofifyMessage( const QString &message ) { mRefreshOnNofifyMessage = message; }
865+
866+
/**
867+
* Returns the message that should be notified by the provider to triggerRepaint
868+
*
869+
* \since QGIS 3.0
870+
*/
871+
QString refreshOnNotifyMessage() const { return mRefreshOnNofifyMessage; }
872+
873+
/**
874+
* Returns true if the refresh on provider nofification is enabled
875+
*
876+
* \since QGIS 3.0
877+
*/
878+
bool isRefreshOnNotifyEnabled() const { return mIsRefreshOnNofifyEnabled; }
879+
880+
850881
signals:
851882

852883
//! 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
931962
*/
932963
void metadataChanged();
933964

965+
private slots:
966+
967+
void onNotifiedTriggerRepaint( const QString &message );
968+
934969
protected:
935970

936971
/** Copies attributes like name, short name, ... into another layer.
@@ -1030,6 +1065,9 @@ class CORE_EXPORT QgsMapLayer : public QObject
10301065
//! Checks whether a new set of dependencies will introduce a cycle
10311066
bool hasDependencyCycle( const QSet<QgsMapLayerDependency> &layers ) const;
10321067

1068+
bool mIsRefreshOnNofifyEnabled = false;
1069+
QString mRefreshOnNofifyMessage;
1070+
10331071
private:
10341072

10351073
/**

‎src/core/qgsvectorlayer.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4434,3 +4434,4 @@ bool QgsVectorLayer::readExtentFromXml() const
44344434
{
44354435
return mReadExtentFromXml;
44364436
}
4437+

‎src/providers/postgres/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ SET(PG_SRCS
1111
qgspgtablemodel.cpp
1212
qgscolumntypethread.cpp
1313
qgspostgresexpressioncompiler.cpp
14+
qgspostgreslistener.cpp
1415
)
1516

1617
SET(PG_MOC_HDRS
@@ -21,7 +22,7 @@ SET(PG_MOC_HDRS
2122
qgspostgresdataitems.h
2223
qgspostgresprovider.h
2324
qgspostgrestransaction.h
24-
25+
qgspostgreslistener.h
2526
)
2627

2728
IF (WITH_GUI)
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/***************************************************************************
2+
qgspostgreslistener.cpp - Listen to postgres NOTIFY
3+
-------------------
4+
begin : Sept 11, 2017
5+
copyright : (C) 2017 by Vincent Mora
6+
email : vincent dor mora at oslandia dot com
7+
***************************************************************************/
8+
9+
/***************************************************************************
10+
* *
11+
* This program is free software; you can redistribute it and/or modify *
12+
* it under the terms of the GNU General Public License as published by *
13+
* the Free Software Foundation; either version 2 of the License, or *
14+
* (at your option) any later version. *
15+
* *
16+
***************************************************************************/
17+
18+
#include "qgspostgreslistener.h"
19+
20+
#include "qgslogger.h"
21+
22+
#ifdef Q_OS_WIN
23+
#include <winsock.h>
24+
#else
25+
#include <sys/select.h>
26+
#endif
27+
28+
extern "C"
29+
{
30+
#include <libpq-fe.h>
31+
}
32+
33+
std::unique_ptr< QgsPostgresListener > QgsPostgresListener::create( const QString &connString )
34+
{
35+
std::unique_ptr< QgsPostgresListener > res( new QgsPostgresListener( connString ) );
36+
QgsDebugMsg( "starting notification listener" );
37+
res->start();
38+
res->mMutex.lock();
39+
res->mIsReadyCondition.wait( &res->mMutex );
40+
res->mMutex.unlock();
41+
42+
return res;
43+
}
44+
45+
QgsPostgresListener::QgsPostgresListener( const QString &connString )
46+
: mConnString( connString )
47+
{
48+
}
49+
50+
QgsPostgresListener::~QgsPostgresListener()
51+
{
52+
mStop = true;
53+
QgsDebugMsg( "stopping the loop" );
54+
wait();
55+
QgsDebugMsg( "notification listener stopped" );
56+
}
57+
58+
void QgsPostgresListener::run()
59+
{
60+
PGconn *conn;
61+
conn = PQconnectdb( mConnString.toLocal8Bit() );
62+
63+
PGresult *res = PQexec( conn, "LISTEN qgis" );
64+
if ( PQresultStatus( res ) != PGRES_COMMAND_OK )
65+
{
66+
QgsDebugMsg( "error in listen" );
67+
PQclear( res );
68+
PQfinish( conn );
69+
mMutex.lock();
70+
mIsReadyCondition.wakeOne();
71+
mMutex.unlock();
72+
return;
73+
}
74+
PQclear( res );
75+
mMutex.lock();
76+
mIsReadyCondition.wakeOne();
77+
mMutex.unlock();
78+
79+
const int sock = PQsocket( conn );
80+
if ( sock < 0 )
81+
{
82+
QgsDebugMsg( "error in socket" );
83+
PQfinish( conn );
84+
return;
85+
}
86+
87+
forever
88+
{
89+
fd_set input_mask;
90+
FD_ZERO( &input_mask );
91+
FD_SET( sock, &input_mask );
92+
93+
timeval timeout;
94+
timeout.tv_sec = 1;
95+
timeout.tv_usec = 0;
96+
97+
QgsDebugMsg( "select in the loop" );
98+
if ( select( sock + 1, &input_mask, nullptr, nullptr, &timeout ) < 0 )
99+
{
100+
QgsDebugMsg( "error in select" );
101+
break;
102+
}
103+
104+
PQconsumeInput( conn );
105+
PGnotify *n = PQnotifies( conn );
106+
if ( n )
107+
{
108+
const QString msg( n->extra );
109+
emit notify( msg );
110+
QgsDebugMsg( "notify " + msg );
111+
PQfreemem( n );
112+
}
113+
else
114+
{
115+
QgsDebugMsg( "not a notify" );
116+
}
117+
118+
if ( mStop )
119+
{
120+
QgsDebugMsg( "stop from main thread" );
121+
break;
122+
}
123+
}
124+
PQfinish( conn );
125+
}
126+
127+
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/***************************************************************************
2+
qgspostgreslistener.h - Listen to postgres NOTIFY
3+
-------------------
4+
begin : Sept 11, 2017
5+
copyright : (C) 2017 by Vincent Mora
6+
email : vincent dor mora at oslandia dot com
7+
***************************************************************************/
8+
9+
/***************************************************************************
10+
* *
11+
* This program is free software; you can redistribute it and/or modify *
12+
* it under the terms of the GNU General Public License as published by *
13+
* the Free Software Foundation; either version 2 of the License, or *
14+
* (at your option) any later version. *
15+
* *
16+
***************************************************************************/
17+
18+
#ifndef QGSPOSTGRESLISTENER_H
19+
#define QGSPOSTGRESLISTENER_H
20+
21+
#include <memory>
22+
23+
#include <QThread>
24+
#include <QWaitCondition>
25+
#include <QMutex>
26+
27+
/**
28+
* \class QgsPostgresListener
29+
* \brief Launch a thread to listen on postgres notifications on the "qgis" channel, the notify signal is emitted on postgres notify.
30+
*
31+
* \since QGIS 3.0
32+
*/
33+
34+
class QgsPostgresListener : public QThread
35+
{
36+
Q_OBJECT
37+
38+
public:
39+
40+
/**
41+
* create an instance if possible and starts the associated thread
42+
* /returns nullptr on error
43+
*/
44+
static std::unique_ptr< QgsPostgresListener > create( const QString &connString );
45+
46+
~QgsPostgresListener();
47+
48+
void run() override;
49+
50+
signals:
51+
void notify( QString message );
52+
53+
private:
54+
volatile bool mStop = false;
55+
const QString mConnString;
56+
QWaitCondition mIsReadyCondition;
57+
QMutex mMutex;
58+
59+
QgsPostgresListener( const QString &connString );
60+
61+
Q_DISABLE_COPY( QgsPostgresListener )
62+
63+
};
64+
65+
#endif // QGSPOSTGRESLISTENER_H

‎src/providers/postgres/qgspostgresprovider.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "qgspostgresdataitems.h"
3636
#include "qgspostgresfeatureiterator.h"
3737
#include "qgspostgrestransaction.h"
38+
#include "qgspostgreslistener.h"
3839
#include "qgslogger.h"
3940
#include "qgsfeedback.h"
4041
#include "qgssettings.h"
@@ -309,6 +310,20 @@ QgsPostgresConn *QgsPostgresProvider::connectionRO() const
309310
return mTransaction ? mTransaction->connection() : mConnectionRO;
310311
}
311312

313+
void QgsPostgresProvider::setListening( bool isListening )
314+
{
315+
if ( isListening && !mListener )
316+
{
317+
mListener.reset( QgsPostgresListener::create( mUri.connectionInfo( false ) ).release() );
318+
connect( mListener.get(), &QgsPostgresListener::notify, this, &QgsPostgresProvider::notify );
319+
}
320+
else if ( !isListening && mListener )
321+
{
322+
disconnect( mListener.get(), &QgsPostgresListener::notify, this, &QgsPostgresProvider::notify );
323+
mListener.reset();
324+
}
325+
}
326+
312327
QgsPostgresConn *QgsPostgresProvider::connectionRW()
313328
{
314329
if ( mTransaction )

‎src/providers/postgres/qgspostgresprovider.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class QgsGeometry;
3232
class QgsPostgresFeatureIterator;
3333
class QgsPostgresSharedData;
3434
class QgsPostgresTransaction;
35+
class QgsPostgresListener;
3536

3637
#include "qgsdatasourceuri.h"
3738

@@ -208,6 +209,14 @@ class QgsPostgresProvider : public QgsVectorDataProvider
208209
*/
209210
virtual bool hasMetadata() const override;
210211

212+
/**
213+
* Launch a listening thead to listen to postgres NOTIFY on "qgis" channel
214+
* the notification is transformed into a Qt signal.
215+
*
216+
* \since QGIS 3.0
217+
*/
218+
void setListening( bool isListening ) override;
219+
211220
signals:
212221

213222
/**
@@ -434,6 +443,8 @@ class QgsPostgresProvider : public QgsVectorDataProvider
434443
QHash<int, QString> mDefaultValues;
435444

436445
bool mCheckPrimaryKeyUnicity = true;
446+
447+
std::unique_ptr< QgsPostgresListener > mListener;
437448
};
438449

439450

‎src/ui/qgsattributeactiondialogbase.ui

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<rect>
77
<x>0</x>
88
<y>0</y>
9-
<width>653</width>
9+
<width>948</width>
1010
<height>731</height>
1111
</rect>
1212
</property>
@@ -44,7 +44,7 @@
4444
<property name="title">
4545
<string>Action list</string>
4646
</property>
47-
<property name="syncGroup" stdset="0">
47+
<property name="syncGroup">
4848
<string notr="true">actiongroup</string>
4949
</property>
5050
<layout class="QGridLayout" name="gridLayout_3">
@@ -146,7 +146,7 @@
146146
<enum>QAbstractItemView::SelectRows</enum>
147147
</property>
148148
<property name="columnCount">
149-
<number>6</number>
149+
<number>7</number>
150150
</property>
151151
<column>
152152
<property name="text">
@@ -178,6 +178,14 @@
178178
<string>Action Scopes</string>
179179
</property>
180180
</column>
181+
<column>
182+
<property name="text">
183+
<string>On Notification</string>
184+
</property>
185+
<property name="toolTip">
186+
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If not empty, this will enable providr notification listening and the action will be executed when hte notification message matched the specified value. &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
187+
</property>
188+
</column>
181189
</widget>
182190
</item>
183191
<item row="1" column="2">

‎src/ui/qgsattributeactionpropertiesdialogbase.ui

Lines changed: 140 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,124 @@
2727
</property>
2828
</widget>
2929
</item>
30+
<item row="5" column="0" colspan="4">
31+
<widget class="QGroupBox" name="mActionGroupBox">
32+
<property name="sizePolicy">
33+
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
34+
<horstretch>0</horstretch>
35+
<verstretch>0</verstretch>
36+
</sizepolicy>
37+
</property>
38+
<property name="title">
39+
<string>Action text</string>
40+
</property>
41+
<layout class="QGridLayout" name="gridLayout">
42+
<item row="1" column="0">
43+
<layout class="QHBoxLayout" name="horizontalLayout_2">
44+
<item>
45+
<widget class="QgsFieldExpressionWidget" name="mFieldExpression">
46+
<property name="focusPolicy">
47+
<enum>Qt::TabFocus</enum>
48+
</property>
49+
</widget>
50+
</item>
51+
<item>
52+
<widget class="QPushButton" name="mInsertFieldOrExpression">
53+
<property name="sizePolicy">
54+
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
55+
<horstretch>0</horstretch>
56+
<verstretch>0</verstretch>
57+
</sizepolicy>
58+
</property>
59+
<property name="toolTip">
60+
<string>Inserts the selected field into the action</string>
61+
</property>
62+
<property name="text">
63+
<string>Insert</string>
64+
</property>
65+
</widget>
66+
</item>
67+
</layout>
68+
</item>
69+
<item row="0" column="0">
70+
<layout class="QGridLayout" name="gridLayout_4">
71+
<item row="2" column="0">
72+
<widget class="QgsCodeEditorPython" name="mActionText" native="true">
73+
<property name="sizePolicy">
74+
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
75+
<horstretch>0</horstretch>
76+
<verstretch>0</verstretch>
77+
</sizepolicy>
78+
</property>
79+
</widget>
80+
</item>
81+
<item row="2" column="1" alignment="Qt::AlignTop">
82+
<widget class="QToolButton" name="mBrowseButton">
83+
<property name="toolTip">
84+
<string>Browse for action</string>
85+
</property>
86+
<property name="statusTip">
87+
<string>Click to browse for an action</string>
88+
</property>
89+
<property name="whatsThis">
90+
<string>Clicking the button will let you select an application to use as the action</string>
91+
</property>
92+
<property name="text">
93+
<string>…</string>
94+
</property>
95+
<property name="icon">
96+
<iconset resource="../../images/images.qrc">
97+
<normaloff>:/images/themes/default/mActionFileOpen.svg</normaloff>:/images/themes/default/mActionFileOpen.svg</iconset>
98+
</property>
99+
</widget>
100+
</item>
101+
<item row="0" column="0" colspan="2">
102+
<widget class="QLabel" name="label_4">
103+
<property name="text">
104+
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The action text defines what happens if the action is triggered.&lt;br/&gt;The content depends on the type.&lt;br/&gt;For the type &lt;span style=&quot; font-style:italic;&quot;&gt;Python&lt;/span&gt; the content should be python code&lt;br/&gt;For other types it should be a file or application with optional parameters&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
105+
</property>
106+
<property name="wordWrap">
107+
<bool>true</bool>
108+
</property>
109+
</widget>
110+
</item>
111+
</layout>
112+
</item>
113+
<item row="2" column="0">
114+
<layout class="QHBoxLayout" name="horizontalLayout_11">
115+
<item>
116+
<widget class="QLabel" name="label_6">
117+
<property name="text">
118+
<string>Execute if notification matches</string>
119+
</property>
120+
</widget>
121+
</item>
122+
<item>
123+
<widget class="QLineEdit" name="mNotificationMessage">
124+
<property name="toolTip">
125+
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If specified, listen to data source notification and performs action if notification message matches the specified value.&lt;/p&gt;&lt;p&gt;E.g. to match messag beginning with &lt;span style=&quot; font-weight:600;&quot;&gt;wathever &lt;/span&gt;use &lt;span style=&quot; font-weight:600;&quot;&gt;^whatever&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
126+
</property>
127+
</widget>
128+
</item>
129+
</layout>
130+
</item>
131+
</layout>
132+
</widget>
133+
</item>
134+
<item row="6" column="0" colspan="4">
135+
<widget class="QDialogButtonBox" name="mButtonBox">
136+
<property name="standardButtons">
137+
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok</set>
138+
</property>
139+
</widget>
140+
</item>
141+
<item row="3" column="0">
142+
<widget class="QLabel" name="label_2">
143+
<property name="text">
144+
<string>Icon</string>
145+
</property>
146+
</widget>
147+
</item>
30148
<item row="3" column="1" colspan="3">
31149
<layout class="QHBoxLayout" name="horizontalLayout_5">
32150
<item>
@@ -64,13 +182,6 @@
64182
</item>
65183
</layout>
66184
</item>
67-
<item row="6" column="0" colspan="4">
68-
<widget class="QDialogButtonBox" name="mButtonBox">
69-
<property name="standardButtons">
70-
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok</set>
71-
</property>
72-
</widget>
73-
</item>
74185
<item row="0" column="0">
75186
<widget class="QLabel" name="label">
76187
<property name="text">
@@ -121,92 +232,34 @@
121232
</item>
122233
</widget>
123234
</item>
124-
<item row="5" column="0" colspan="4">
125-
<widget class="QGroupBox" name="mActionGroupBox">
126-
<property name="sizePolicy">
127-
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
128-
<horstretch>0</horstretch>
129-
<verstretch>0</verstretch>
130-
</sizepolicy>
235+
<item row="4" column="0" colspan="3">
236+
<widget class="QGroupBox" name="mActionScopesGroupBox">
237+
<property name="focusPolicy">
238+
<enum>Qt::StrongFocus</enum>
131239
</property>
132240
<property name="title">
133-
<string>Action text</string>
241+
<string>Action Scopes</string>
134242
</property>
135-
<layout class="QGridLayout" name="gridLayout">
136-
<item row="1" column="0">
137-
<layout class="QHBoxLayout" name="horizontalLayout_2">
138-
<item>
139-
<widget class="QgsFieldExpressionWidget" name="mFieldExpression" native="true">
140-
<property name="focusPolicy">
141-
<enum>Qt::TabFocus</enum>
142-
</property>
143-
</widget>
144-
</item>
145-
<item>
146-
<widget class="QPushButton" name="mInsertFieldOrExpression">
147-
<property name="sizePolicy">
148-
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
149-
<horstretch>0</horstretch>
150-
<verstretch>0</verstretch>
151-
</sizepolicy>
152-
</property>
153-
<property name="toolTip">
154-
<string>Inserts the selected field into the action</string>
155-
</property>
156-
<property name="text">
157-
<string>Insert</string>
158-
</property>
159-
</widget>
160-
</item>
161-
</layout>
162-
</item>
163-
<item row="0" column="0">
164-
<layout class="QGridLayout" name="gridLayout_4">
165-
<item row="2" column="0">
166-
<widget class="QgsCodeEditorPython" name="mActionText" native="true">
167-
<property name="sizePolicy">
168-
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
169-
<horstretch>0</horstretch>
170-
<verstretch>0</verstretch>
171-
</sizepolicy>
172-
</property>
173-
</widget>
174-
</item>
175-
<item row="2" column="1" alignment="Qt::AlignTop">
176-
<widget class="QToolButton" name="mBrowseButton">
177-
<property name="toolTip">
178-
<string>Browse for action</string>
179-
</property>
180-
<property name="statusTip">
181-
<string>Click to browse for an action</string>
182-
</property>
183-
<property name="whatsThis">
184-
<string>Clicking the button will let you select an application to use as the action</string>
185-
</property>
186-
<property name="text">
187-
<string>…</string>
188-
</property>
189-
<property name="icon">
190-
<iconset resource="../../images/images.qrc">
191-
<normaloff>:/images/themes/default/mActionFileOpen.svg</normaloff>:/images/themes/default/mActionFileOpen.svg</iconset>
192-
</property>
193-
</widget>
194-
</item>
195-
<item row="0" column="0" colspan="2">
196-
<widget class="QLabel" name="label_4">
197-
<property name="text">
198-
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The action text defines what happens if the action is triggered.&lt;br/&gt;The content depends on the type.&lt;br/&gt;For the type &lt;span style=&quot; font-style:italic;&quot;&gt;Python&lt;/span&gt; the content should be python code&lt;br/&gt;For other types it should be a file or application with optional parameters&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
199-
</property>
200-
<property name="wordWrap">
201-
<bool>true</bool>
202-
</property>
203-
</widget>
204-
</item>
205-
</layout>
206-
</item>
243+
<layout class="QFormLayout" name="formLayout">
244+
<property name="fieldGrowthPolicy">
245+
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
246+
</property>
207247
</layout>
208248
</widget>
209249
</item>
250+
<item row="1" column="1" colspan="3">
251+
<widget class="QLineEdit" name="mActionName">
252+
<property name="toolTip">
253+
<string>Enter the action name here</string>
254+
</property>
255+
<property name="whatsThis">
256+
<string>Enter the name of an action here. The name should be unique (QGIS will make it unique if necessary).</string>
257+
</property>
258+
<property name="placeholderText">
259+
<string>Mandatory description</string>
260+
</property>
261+
</widget>
262+
</item>
210263
<item row="2" column="0">
211264
<widget class="QLabel" name="label_3">
212265
<property name="text">
@@ -234,37 +287,6 @@
234287
</property>
235288
</widget>
236289
</item>
237-
<item row="3" column="0">
238-
<widget class="QLabel" name="label_2">
239-
<property name="text">
240-
<string>Icon</string>
241-
</property>
242-
</widget>
243-
</item>
244-
<item row="1" column="1" colspan="3">
245-
<widget class="QLineEdit" name="mActionName">
246-
<property name="toolTip">
247-
<string>Enter the action name here</string>
248-
</property>
249-
<property name="whatsThis">
250-
<string>Enter the name of an action here. The name should be unique (QGIS will make it unique if necessary).</string>
251-
</property>
252-
<property name="placeholderText">
253-
<string>Mandatory description</string>
254-
</property>
255-
</widget>
256-
</item>
257-
<item row="4" column="0" colspan="3">
258-
<widget class="QGroupBox" name="mActionScopesGroupBox">
259-
<property name="focusPolicy">
260-
<enum>Qt::StrongFocus</enum>
261-
</property>
262-
<property name="title">
263-
<string>Action Scopes</string>
264-
</property>
265-
<layout class="QFormLayout" name="formLayout"/>
266-
</widget>
267-
</item>
268290
</layout>
269291
</widget>
270292
<customwidgets>

‎src/ui/qgsvectorlayerpropertiesbase.ui

Lines changed: 152 additions & 299 deletions
Large diffs are not rendered by default.

‎tests/src/python/test_provider_postgres.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import psycopg2
1818

1919
import os
20+
import time
2021

2122
from qgis.core import (
2223
QgsVectorLayer,
@@ -33,13 +34,13 @@
3334
QgsRectangle
3435
)
3536
from qgis.gui import QgsGui
36-
from qgis.PyQt.QtCore import QDate, QTime, QDateTime, QVariant, QDir
37+
from qgis.PyQt.QtCore import QDate, QTime, QDateTime, QVariant, QDir, QObject
3738
from qgis.testing import start_app, unittest
3839
from qgis.PyQt.QtXml import QDomDocument
3940
from utilities import unitTestDataPath
4041
from providertestbase import ProviderTestCase
4142

42-
start_app()
43+
QGISAPP = start_app()
4344
TEST_DATA_DIR = unitTestDataPath()
4445

4546

@@ -809,6 +810,42 @@ def testReadExtentOnTable(self):
809810

810811
self.assertEqual(vl2.extent(), originalExtent)
811812

813+
def testNotify(self):
814+
vl0 = QgsVectorLayer(self.dbconn + ' sslmode=disable key=\'pk\' srid=4326 type=POLYGON table="qgis_test"."some_poly_data" (geom) sql=', 'test', 'postgres')
815+
vl0.dataProvider().setListening(True)
816+
817+
class Notified(QObject):
818+
819+
def __init__(self):
820+
super(Notified, self).__init__()
821+
self.received = ""
822+
823+
def receive(self, msg):
824+
self.received = msg
825+
826+
notified = Notified()
827+
vl0.dataProvider().notify.connect(notified.receive)
828+
829+
vl0.dataProvider().setListening(True)
830+
831+
cur = self.con.cursor()
832+
ok = False
833+
start = time.time()
834+
while True:
835+
cur.execute("NOTIFY qgis, 'my message'")
836+
self.con.commit()
837+
QGISAPP.processEvents()
838+
if notified.received == "my message":
839+
ok = True
840+
break
841+
if (time.time() - start) > 5: # timeout
842+
break
843+
844+
vl0.dataProvider().notify.disconnect(notified.receive)
845+
vl0.dataProvider().setListening(False)
846+
847+
self.assertTrue(ok)
848+
812849

813850
class TestPyQgsPostgresProviderCompoundKey(unittest.TestCase, ProviderTestCase):
814851

0 commit comments

Comments
 (0)
Please sign in to comment.