QGIS API Documentation  3.18.1-Zürich (202f1bf7e5)
qgsvaluerelationwidgetwrapper.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsvaluerelationwidgetwrapper.cpp
3  --------------------------------------
4  Date : 5.1.2014
5  Copyright : (C) 2014 Matthias Kuhn
6  Email : matthias at opengis dot ch
7  ***************************************************************************
8  * *
9  * This program is free software; you can redistribute it and/or modify *
10  * it under the terms of the GNU General Public License as published by *
11  * the Free Software Foundation; either version 2 of the License, or *
12  * (at your option) any later version. *
13  * *
14  ***************************************************************************/
15 
17 
18 #include "qgis.h"
19 #include "qgsfields.h"
20 #include "qgsproject.h"
22 #include "qgsvectorlayer.h"
23 #include "qgsfilterlineedit.h"
24 #include "qgsfeatureiterator.h"
26 #include "qgsattributeform.h"
27 #include "qgsattributes.h"
28 #include "qgsjsonutils.h"
29 #include "qgspostgresstringutils.h"
30 
31 #include <QHeaderView>
32 #include <QComboBox>
33 #include <QLineEdit>
34 #include <QTableWidget>
35 #include <QStringListModel>
36 #include <QCompleter>
37 #include <QTimer>
38 
39 #include <nlohmann/json.hpp>
40 using namespace nlohmann;
41 
42 
43 QgsValueRelationWidgetWrapper::QgsValueRelationWidgetWrapper( QgsVectorLayer *layer, int fieldIdx, QWidget *editor, QWidget *parent )
44  : QgsEditorWidgetWrapper( layer, fieldIdx, editor, parent )
45 {
46 }
47 
48 
50 {
51  QVariant v;
52 
53  if ( mComboBox )
54  {
55  int cbxIdx = mComboBox->currentIndex();
56  if ( cbxIdx > -1 )
57  {
58  v = mComboBox->currentData();
59  }
60  }
61 
62  const int nofColumns = columnCount();
63 
64  if ( mTableWidget )
65  {
66  QStringList selection;
67  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
68  {
69  for ( int i = 0; i < nofColumns; ++i )
70  {
71  QTableWidgetItem *item = mTableWidget->item( j, i );
72  if ( item )
73  {
74  if ( item->checkState() == Qt::Checked )
75  selection << item->data( Qt::UserRole ).toString();
76  }
77  }
78  }
79 
80  QVariantList vl;
81  //store as QVariantList because the field type supports data structure
82  for ( const QString &s : qgis::as_const( selection ) )
83  {
84  // Convert to proper type
85  const QVariant::Type type { fkType() };
86  switch ( type )
87  {
88  case QVariant::Type::Int:
89  vl.push_back( s.toInt() );
90  break;
91  case QVariant::Type::LongLong:
92  vl.push_back( s.toLongLong() );
93  break;
94  default:
95  vl.push_back( s );
96  break;
97  }
98  }
99 
100  if ( layer()->fields().at( fieldIdx() ).type() == QVariant::Map ||
101  layer()->fields().at( fieldIdx() ).type() == QVariant::List )
102  {
103  v = vl;
104  }
105  else
106  {
107  //make string
109  }
110  }
111 
112  if ( mLineEdit )
113  {
114  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &item : qgis::as_const( mCache ) )
115  {
116  if ( item.value == mLineEdit->text() )
117  {
118  v = item.key;
119  break;
120  }
121  }
122  }
123 
124  return v;
125 }
126 
128 {
129  QgsAttributeForm *form = qobject_cast<QgsAttributeForm *>( parent );
130  if ( form )
132 
133  mExpression = config().value( QStringLiteral( "FilterExpression" ) ).toString();
134 
135  if ( config( QStringLiteral( "AllowMulti" ) ).toBool() )
136  {
137  return new QTableWidget( parent );
138  }
139  else if ( config( QStringLiteral( "UseCompleter" ) ).toBool() )
140  {
141  return new QgsFilterLineEdit( parent );
142  }
143  else
144  {
145  return new QComboBox( parent );
146  }
147 }
148 
150 {
151 
152  mComboBox = qobject_cast<QComboBox *>( editor );
153  mTableWidget = qobject_cast<QTableWidget *>( editor );
154  mLineEdit = qobject_cast<QLineEdit *>( editor );
155 
156  // Read current initial form values from the editor context
158 
159  if ( mComboBox )
160  {
161  connect( mComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ),
162  this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection );
163  }
164  else if ( mTableWidget )
165  {
166  mTableWidget->horizontalHeader()->setSectionResizeMode( QHeaderView::Stretch );
167  mTableWidget->horizontalHeader()->setVisible( false );
168  mTableWidget->verticalHeader()->setSectionResizeMode( QHeaderView::Stretch );
169  mTableWidget->verticalHeader()->setVisible( false );
170  mTableWidget->setShowGrid( false );
171  mTableWidget->setEditTriggers( QAbstractItemView::NoEditTriggers );
172  mTableWidget->setSelectionMode( QAbstractItemView::NoSelection );
173  connect( mTableWidget, &QTableWidget::itemChanged, this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection );
174  }
175  else if ( mLineEdit )
176  {
177  connect( mLineEdit, &QLineEdit::textChanged, this, &QgsValueRelationWidgetWrapper::emitValueChangedInternal, Qt::UniqueConnection );
178  }
179 }
180 
182 {
183  return mTableWidget || mLineEdit || mComboBox;
184 }
185 
186 void QgsValueRelationWidgetWrapper::updateValues( const QVariant &value, const QVariantList & )
187 {
188  if ( mTableWidget )
189  {
190  QStringList checkList;
191 
192  if ( layer()->fields().at( fieldIdx() ).type() == QVariant::Map ||
193  layer()->fields().at( fieldIdx() ).type() == QVariant::List )
194  {
195  checkList = value.toStringList();
196  }
197  else
198  {
200  }
201 
202  QTableWidgetItem *lastChangedItem = nullptr;
203 
204  const int nofColumns = columnCount();
205 
206  // This block is needed because item->setCheckState triggers dataChanged gets back to value()
207  // and iterate over all items again! This can be extremely slow on large items sets.
208  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
209  {
210  auto signalBlockedTableWidget = whileBlocking( mTableWidget );
211  Q_UNUSED( signalBlockedTableWidget )
212 
213  for ( int i = 0; i < nofColumns; ++i )
214  {
215  QTableWidgetItem *item = mTableWidget->item( j, i );
216  if ( item )
217  {
218  item->setCheckState( checkList.contains( item->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked );
219  //re-set enabled state because it's lost after reloading items
220  item->setFlags( mEnabled ? item->flags() | Qt::ItemIsEnabled : item->flags() & ~Qt::ItemIsEnabled );
221  lastChangedItem = item;
222  }
223  }
224  }
225  // let's trigger the signal now, once and for all
226  if ( lastChangedItem )
227  lastChangedItem->setCheckState( checkList.contains( lastChangedItem->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked );
228 
229  }
230  else if ( mComboBox )
231  {
232  // findData fails to tell a 0 from a NULL
233  // See: "Value relation, value 0 = NULL" - https://github.com/qgis/QGIS/issues/27803
234  int idx = -1; // default to not found
235  for ( int i = 0; i < mComboBox->count(); i++ )
236  {
237  QVariant v( mComboBox->itemData( i ) );
238  if ( qgsVariantEqual( v, value ) )
239  {
240  idx = i;
241  break;
242  }
243  }
244  mComboBox->setCurrentIndex( idx );
245  }
246  else if ( mLineEdit )
247  {
248  mLineEdit->clear();
249  bool wasFound { false };
250  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : qgis::as_const( mCache ) )
251  {
252  if ( i.key == value )
253  {
254  mLineEdit->setText( i.value );
255  wasFound = true;
256  break;
257  }
258  }
259  // Value could not be found
260  if ( ! wasFound )
261  {
262  mLineEdit->setText( tr( "(no selection)" ) );
263  }
264  }
265 }
266 
267 void QgsValueRelationWidgetWrapper::widgetValueChanged( const QString &attribute, const QVariant &newValue, bool attributeChanged )
268 {
269 
270  // Do nothing if the value has not changed
271  if ( attributeChanged )
272  {
273  QVariant oldValue( value( ) );
274  setFormFeatureAttribute( attribute, newValue );
275  // Update combos if the value used in the filter expression has changed
277  && QgsValueRelationFieldFormatter::expressionFormAttributes( mExpression ).contains( attribute ) )
278  {
279  populate();
280  // Restore value
281  updateValues( value( ) );
282  // If the value has changed as a result of another widget's value change,
283  // we need to emit the signal to make sure other dependent widgets are
284  // updated.
285  QgsFields formFields( formFeature().fields() );
286 
287  // Also check for fields in the layer in case this is a multi-edit form
288  // and there is not form feature set
289  if ( formFields.count() == 0 && layer() )
290  {
291  formFields = layer()->fields();
292  }
293 
294  if ( oldValue != value() && fieldIdx() < formFields.count() )
295  {
296  QString attributeName( formFields.names().at( fieldIdx() ) );
297  setFormFeatureAttribute( attributeName, value( ) );
299  }
300  }
301  }
302 }
303 
304 
306 {
307  setFormFeature( feature );
308  whileBlocking( this )->populate();
309  whileBlocking( this )->setValue( feature.attribute( fieldIdx() ) );
310 
311  // As we block any signals, possible depending widgets will not being updated
312  // so we force emit signal once and for all
314 
315  // A bit of logic to set the default value if AllowNull is false and this is a new feature
316  // Note that this needs to be here after the cache has been created/updated by populate()
317  // and signals unblocked (we want this to propagate to the feature itself)
318  if ( context().attributeFormMode() != QgsAttributeEditorContext::Mode::MultiEditMode
319  && ! formFeature().attribute( fieldIdx() ).isValid()
320  && ! mCache.isEmpty()
321  && ! config( QStringLiteral( "AllowNull" ) ).toBool( ) )
322  {
323  // This is deferred because at the time the feature is set in one widget it is not
324  // set in the next, which is typically the "down" in a drill-down
325  QTimer::singleShot( 0, this, [ this ]
326  {
327  if ( ! mCache.isEmpty() )
328  {
329  updateValues( formFeature().attribute( fieldIdx() ).isValid() ? formFeature().attribute( fieldIdx() ) : mCache.at( 0 ).key );
330  }
331  } );
332  }
333 }
334 
335 int QgsValueRelationWidgetWrapper::columnCount() const
336 {
337  return std::max( 1, config( QStringLiteral( "NofColumns" ) ).toInt() );
338 }
339 
340 
341 QVariant::Type QgsValueRelationWidgetWrapper::fkType() const
342 {
344  if ( layer )
345  {
346  QgsFields fields = layer->fields();
347  int idx { fields.lookupField( config().value( QStringLiteral( "Key" ) ).toString() ) };
348  if ( idx >= 0 )
349  {
350  return fields.at( idx ).type();
351  }
352  }
353  return QVariant::Type::Invalid;
354 }
355 
356 void QgsValueRelationWidgetWrapper::populate( )
357 {
358  // Initialize, note that signals are blocked, to avoid double signals on new features
361  {
362  if ( context().parentFormFeature().isValid() )
363  {
364  mCache = QgsValueRelationFieldFormatter::createCache( config(), formFeature(), context().parentFormFeature() );
365  }
366  else
367  {
369  }
370  }
371  else if ( mCache.empty() )
372  {
374  }
375 
376  if ( mComboBox )
377  {
378  mComboBox->clear();
379  if ( config( QStringLiteral( "AllowNull" ) ).toBool( ) )
380  {
381  whileBlocking( mComboBox )->addItem( tr( "(no selection)" ), QVariant( field().type( ) ) );
382  }
383 
384  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : qgis::as_const( mCache ) )
385  {
386  whileBlocking( mComboBox )->addItem( element.value, element.key );
387  if ( !element.description.isEmpty() )
388  mComboBox->setItemData( mComboBox->count() - 1, element.description, Qt::ToolTipRole );
389  }
390 
391  }
392  else if ( mTableWidget )
393  {
394  const int nofColumns = columnCount();
395 
396  if ( ! mCache.empty() )
397  {
398  mTableWidget->setRowCount( ( mCache.size() + nofColumns - 1 ) / nofColumns );
399  }
400  else
401  mTableWidget->setRowCount( 1 );
402  mTableWidget->setColumnCount( nofColumns );
403 
404  whileBlocking( mTableWidget )->clear();
405  int row = 0;
406  int column = 0;
407  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : qgis::as_const( mCache ) )
408  {
409  if ( column == nofColumns )
410  {
411  row++;
412  column = 0;
413  }
414  QTableWidgetItem *item = nullptr;
415  item = new QTableWidgetItem( element.value );
416  item->setData( Qt::UserRole, element.key );
417  whileBlocking( mTableWidget )->setItem( row, column, item );
418  column++;
419  }
420 
421  }
422  else if ( mLineEdit )
423  {
424  QStringList values;
425  values.reserve( mCache.size() );
426  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : qgis::as_const( mCache ) )
427  {
428  values << i.value;
429  }
430  QStringListModel *m = new QStringListModel( values, mLineEdit );
431  QCompleter *completer = new QCompleter( m, mLineEdit );
432  completer->setCaseSensitivity( Qt::CaseInsensitive );
433  mLineEdit->setCompleter( completer );
434  }
435 }
436 
438 {
439  const int nofColumns = columnCount();
440 
441  if ( mTableWidget )
442  {
443  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
444  {
445  for ( int i = 0; i < nofColumns; ++i )
446  {
447  whileBlocking( mTableWidget )->item( j, i )->setCheckState( Qt::PartiallyChecked );
448  }
449  }
450  }
451  else if ( mComboBox )
452  {
453  whileBlocking( mComboBox )->setCurrentIndex( -1 );
454  }
455  else if ( mLineEdit )
456  {
457  whileBlocking( mLineEdit )->clear();
458  }
459 }
460 
462 {
463  if ( mEnabled == enabled )
464  return;
465 
466  mEnabled = enabled;
467 
468  if ( mTableWidget )
469  {
470  auto signalBlockedTableWidget = whileBlocking( mTableWidget );
471  Q_UNUSED( signalBlockedTableWidget )
472 
473  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
474  {
475  for ( int i = 0; i < mTableWidget->columnCount(); ++i )
476  {
477  QTableWidgetItem *item = mTableWidget->item( j, i );
478  if ( item )
479  {
480  item->setFlags( enabled ? item->flags() | Qt::ItemIsEnabled : item->flags() & ~Qt::ItemIsEnabled );
481  }
482  }
483  }
484  }
485  else
487 }
488 
489 void QgsValueRelationWidgetWrapper::parentFormValueChanged( const QString &attribute, const QVariant &value )
490 {
491 
492  // Update the parent feature in the context ( which means to replace the whole context :/ )
494  QgsFeature feature { context().parentFormFeature() };
495  feature.setAttribute( attribute, value );
496  ctx.setParentFormFeature( feature );
497  setContext( ctx );
498 
499  // Check if the change might affect the filter expression and the cache needs updates
501  && ( config( QStringLiteral( "Value" ) ).toString() == attribute ||
502  config( QStringLiteral( "Key" ) ).toString() == attribute ||
504  QgsValueRelationFieldFormatter::expressionParentFormAttributes( mExpression ).contains( attribute ) ) )
505  {
506  populate();
507  }
508 
509 }
510 
511 void QgsValueRelationWidgetWrapper::emitValueChangedInternal( const QString &value )
512 {
514  emit valueChanged( value );
516  emit valuesChanged( value );
517 }
This class contains context information for attribute editor widgets.
QgsFeature parentFormFeature() const
Returns the feature of the currently edited parent form in its actual state.
void widgetValueChanged(const QString &attribute, const QVariant &value, bool attributeChanged)
Notifies about changes of attributes.
Manages an editor widget Widget and wrapper share the same parent.
QgsFeature formFeature() const
The feature currently being edited, in its current state.
Q_DECL_DEPRECATED void valueChanged(const QVariant &value)
Emit this signal, whenever the value changed.
void setFormFeature(const QgsFeature &feature)
Set the feature currently being edited to feature.
int fieldIdx() const
Access the field index.
void setEnabled(bool enabled) override
Is used to enable or disable the edit functionality of the managed widget.
void valuesChanged(const QVariant &value, const QVariantList &additionalFieldValues=QVariantList())
Emit this signal, whenever the value changed.
bool setFormFeatureAttribute(const QString &attributeName, const QVariant &attributeValue)
Update the feature currently being edited by changing its attribute attributeName to attributeValue.
void emitValueChanged()
Will call the value() method to determine the emitted value.
QgsField field() const
Access the field.
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:56
bool setAttribute(int field, const QVariant &attr)
Set an attribute's value by field index.
Definition: qgsfeature.cpp:236
QVariant attribute(const QString &name) const
Lookup attribute value from attribute name.
Definition: qgsfeature.cpp:287
QVariant::Type type
Definition: qgsfield.h:58
Container of fields for a vector layer.
Definition: qgsfields.h:45
int count() const
Returns number of items.
Definition: qgsfields.cpp:133
QgsField at(int i) const
Gets field at particular index (must be in range 0..N-1)
Definition: qgsfields.cpp:163
int lookupField(const QString &fieldName) const
Looks up field's index from the field name.
Definition: qgsfields.cpp:344
QStringList names() const
Returns a list with field names.
Definition: qgsfields.cpp:143
QLineEdit subclass with built in support for clearing the widget's value and handling custom null val...
static QString buildArray(const QVariantList &list)
Build a postgres array like formatted list in a string from a QVariantList.
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:501
static QgsVectorLayer * resolveLayer(const QVariantMap &config, const QgsProject *project)
Returns the (possibly NULL) layer from the widget's config and project.
static bool expressionRequiresFormScope(const QString &expression)
Check if the expression requires a form scope (i.e.
static bool expressionRequiresParentFormScope(const QString &expression)
Check if the expression requires a parent form scope (i.e.
QVariant createCache(QgsVectorLayer *layer, int fieldIndex, const QVariantMap &config) const override
Create a cache for a given field.
static QSet< QString > expressionParentFormVariables(const QString &expression)
Returns a list of variables required by the parent form's form context expression.
static QSet< QString > expressionParentFormAttributes(const QString &expression)
Returns a list of attributes required by the parent form's form context expression.
static QStringList valueToStringList(const QVariant &value)
Utility to convert a list or a string representation of an (hstore style: {1,2...}) list in value to ...
static QSet< QString > expressionFormAttributes(const QString &expression)
Returns a list of attributes required by the form context expression.
QVariant value() const override
Will be used to access the widget's value.
bool valid() const override
Returns true if the widget has been properly initialized.
void showIndeterminateState() override
Sets the widget to display in an indeterminate "mixed value" state.
void parentFormValueChanged(const QString &attribute, const QVariant &value) override
QgsValueRelationWidgetWrapper(QgsVectorLayer *layer, int fieldIdx, QWidget *editor=nullptr, QWidget *parent=nullptr)
Constructor for QgsValueRelationWidgetWrapper.
void setEnabled(bool enabled) override
Is used to enable or disable the edit functionality of the managed widget.
void widgetValueChanged(const QString &attribute, const QVariant &newValue, bool attributeChanged)
Will be called when a value in the current edited form or table row changes.
QWidget * createWidget(QWidget *parent) override
This method should create a new widget with the provided parent.
void setFeature(const QgsFeature &feature) override
Will be called when the feature changes.
void initWidget(QWidget *editor) override
This method should initialize the editor widget with runtime data.
Represents a vector layer which manages a vector based data sets.
QgsFields fields() const FINAL
Returns the list of fields of this layer.
const QgsAttributeEditorContext & context() const
Returns information about the context in which this widget is shown.
QgsVectorLayer * layer() const
Returns the vector layer associated with the widget.
void setContext(const QgsAttributeEditorContext &context)
Set the context in which this widget is shown.
QVariantMap config() const
Returns the whole config.
bool qgsVariantEqual(const QVariant &lhs, const QVariant &rhs)
Compares two QVariant values and returns whether they are equal, two NULL values are always treated a...
Definition: qgis.cpp:265
#define Q_NOWARN_DEPRECATED_POP
Definition: qgis.h:798
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:797
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition: qgis.h:263