QGIS API Documentation  3.24.2-Tisler (13c1a02865)
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 #include "qgsapplication.h"
31 
32 #include <QHeaderView>
33 #include <QComboBox>
34 #include <QLineEdit>
35 #include <QTableWidget>
36 #include <QStringListModel>
37 #include <QCompleter>
38 #include <QTimer>
39 
40 #include <nlohmann/json.hpp>
41 using namespace nlohmann;
42 
43 
44 QgsValueRelationWidgetWrapper::QgsValueRelationWidgetWrapper( QgsVectorLayer *layer, int fieldIdx, QWidget *editor, QWidget *parent )
45  : QgsEditorWidgetWrapper( layer, fieldIdx, editor, parent )
46 {
47 }
48 
49 
51 {
52  QVariant v;
53 
54  if ( mComboBox )
55  {
56  int cbxIdx = mComboBox->currentIndex();
57  if ( cbxIdx > -1 )
58  {
59  v = mComboBox->currentData();
60  if ( v.isNull() )
61  v = QVariant( field().type() );
62  }
63  }
64  else if ( mTableWidget )
65  {
66  const int nofColumns = columnCount();
67  QStringList selection;
68  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
69  {
70  for ( int i = 0; i < nofColumns; ++i )
71  {
72  QTableWidgetItem *item = mTableWidget->item( j, i );
73  if ( item )
74  {
75  if ( item->checkState() == Qt::Checked )
76  selection << item->data( Qt::UserRole ).toString();
77  }
78  }
79  }
80 
81  // If there is no selection and allow NULL is not checked return NULL.
82  if ( selection.isEmpty() && ! config( QStringLiteral( "AllowNull" ) ).toBool( ) )
83  {
84  return QVariant( QVariant::Type::List );
85  }
86 
87  QVariantList vl;
88  //store as QVariantList because the field type supports data structure
89  for ( const QString &s : std::as_const( selection ) )
90  {
91  // Convert to proper type
92  const QVariant::Type type { fkType() };
93  switch ( type )
94  {
95  case QVariant::Type::Int:
96  vl.push_back( s.toInt() );
97  break;
98  case QVariant::Type::LongLong:
99  vl.push_back( s.toLongLong() );
100  break;
101  default:
102  vl.push_back( s );
103  break;
104  }
105  }
106 
107  if ( layer()->fields().at( fieldIdx() ).type() == QVariant::Map ||
108  layer()->fields().at( fieldIdx() ).type() == QVariant::List )
109  {
110  v = vl;
111  }
112  else
113  {
114  //make string
116  }
117  }
118  else if ( mLineEdit )
119  {
120  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &item : std::as_const( mCache ) )
121  {
122  if ( item.value == mLineEdit->text() )
123  {
124  v = item.key;
125  break;
126  }
127  }
128  }
129 
130  return v;
131 }
132 
134 {
135  QgsAttributeForm *form = qobject_cast<QgsAttributeForm *>( parent );
136  if ( form )
138 
139  mExpression = config().value( QStringLiteral( "FilterExpression" ) ).toString();
140 
141  if ( config( QStringLiteral( "AllowMulti" ) ).toBool() )
142  {
143  return new QTableWidget( parent );
144  }
145  else if ( config( QStringLiteral( "UseCompleter" ) ).toBool() )
146  {
147  return new QgsFilterLineEdit( parent );
148  }
149  else
150  {
151  return new QComboBox( parent );
152  }
153 }
154 
156 {
157 
158  mComboBox = qobject_cast<QComboBox *>( editor );
159  mTableWidget = qobject_cast<QTableWidget *>( editor );
160  mLineEdit = qobject_cast<QLineEdit *>( editor );
161 
162  // Read current initial form values from the editor context
164 
165  if ( mComboBox )
166  {
167  mComboBox->view()->setVerticalScrollBarPolicy( Qt::ScrollBarAsNeeded );
168  connect( mComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ),
169  this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection );
170  }
171  else if ( mTableWidget )
172  {
173  mTableWidget->horizontalHeader()->setSectionResizeMode( QHeaderView::Stretch );
174  mTableWidget->horizontalHeader()->setVisible( false );
175  mTableWidget->verticalHeader()->setSectionResizeMode( QHeaderView::Stretch );
176  mTableWidget->verticalHeader()->setVisible( false );
177  mTableWidget->setShowGrid( false );
178  mTableWidget->setEditTriggers( QAbstractItemView::NoEditTriggers );
179  mTableWidget->setSelectionMode( QAbstractItemView::NoSelection );
180  connect( mTableWidget, &QTableWidget::itemChanged, this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection );
181  }
182  else if ( mLineEdit )
183  {
184  connect( mLineEdit, &QLineEdit::textChanged, this, &QgsValueRelationWidgetWrapper::emitValueChangedInternal, Qt::UniqueConnection );
185  }
186 }
187 
189 {
190  return mTableWidget || mLineEdit || mComboBox;
191 }
192 
193 void QgsValueRelationWidgetWrapper::updateValues( const QVariant &value, const QVariantList & )
194 {
195  if ( mTableWidget )
196  {
197  QStringList checkList;
198 
199  if ( layer()->fields().at( fieldIdx() ).type() == QVariant::Map ||
200  layer()->fields().at( fieldIdx() ).type() == QVariant::List )
201  {
202  checkList = value.toStringList();
203  }
204  else
205  {
207  }
208 
209  QTableWidgetItem *lastChangedItem = nullptr;
210 
211  const int nofColumns = columnCount();
212 
213  // This block is needed because item->setCheckState triggers dataChanged gets back to value()
214  // and iterate over all items again! This can be extremely slow on large items sets.
215  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
216  {
217  auto signalBlockedTableWidget = whileBlocking( mTableWidget );
218  Q_UNUSED( signalBlockedTableWidget )
219 
220  for ( int i = 0; i < nofColumns; ++i )
221  {
222  QTableWidgetItem *item = mTableWidget->item( j, i );
223  if ( item )
224  {
225  item->setCheckState( checkList.contains( item->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked );
226  //re-set enabled state because it's lost after reloading items
227  item->setFlags( mEnabled ? item->flags() | Qt::ItemIsEnabled : item->flags() & ~Qt::ItemIsEnabled );
228  lastChangedItem = item;
229  }
230  }
231  }
232  // let's trigger the signal now, once and for all
233  if ( lastChangedItem )
234  lastChangedItem->setCheckState( checkList.contains( lastChangedItem->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked );
235 
236  }
237  else if ( mComboBox )
238  {
239  // findData fails to tell a 0 from a NULL
240  // See: "Value relation, value 0 = NULL" - https://github.com/qgis/QGIS/issues/27803
241  int idx = -1; // default to not found
242  for ( int i = 0; i < mComboBox->count(); i++ )
243  {
244  QVariant v( mComboBox->itemData( i ) );
245  if ( qgsVariantEqual( v, value ) )
246  {
247  idx = i;
248  break;
249  }
250  }
251 
252  if ( idx == -1 )
253  {
254  // if value doesn't exist, we show it in '(...)' (just like value map widget)
255  if ( value.isNull( ) )
256  {
257  mComboBox->setCurrentIndex( -1 );
258  }
259  else
260  {
261  mComboBox->addItem( value.toString().prepend( '(' ).append( ')' ), value );
262  mComboBox->setCurrentIndex( mComboBox->findData( value ) );
263  }
264  }
265  else
266  {
267  mComboBox->setCurrentIndex( idx );
268  }
269  }
270  else if ( mLineEdit )
271  {
272  mLineEdit->clear();
273  bool wasFound { false };
274  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : std::as_const( mCache ) )
275  {
276  if ( i.key == value )
277  {
278  mLineEdit->setText( i.value );
279  wasFound = true;
280  break;
281  }
282  }
283  // Value could not be found
284  if ( ! wasFound )
285  {
286  mLineEdit->setText( tr( "(no selection)" ) );
287  }
288  }
289 }
290 
291 void QgsValueRelationWidgetWrapper::widgetValueChanged( const QString &attribute, const QVariant &newValue, bool attributeChanged )
292 {
293 
294  // Do nothing if the value has not changed
295  if ( attributeChanged )
296  {
297  QVariant oldValue( value( ) );
298  setFormFeatureAttribute( attribute, newValue );
299  // Update combos if the value used in the filter expression has changed
301  && QgsValueRelationFieldFormatter::expressionFormAttributes( mExpression ).contains( attribute ) )
302  {
303  populate();
304  // Restore value
305  updateValues( value( ) );
306  // If the value has changed as a result of another widget's value change,
307  // we need to emit the signal to make sure other dependent widgets are
308  // updated.
309  QgsFields formFields( formFeature().fields() );
310 
311  // Also check for fields in the layer in case this is a multi-edit form
312  // and there is not form feature set
313  if ( formFields.count() == 0 && layer() )
314  {
315  formFields = layer()->fields();
316  }
317 
318  if ( oldValue != value() && fieldIdx() < formFields.count() )
319  {
320  QString attributeName( formFields.names().at( fieldIdx() ) );
321  setFormFeatureAttribute( attributeName, value( ) );
323  }
324  }
325  }
326 }
327 
328 
330 {
331  setFormFeature( feature );
332  whileBlocking( this )->populate();
333  whileBlocking( this )->setValue( feature.attribute( fieldIdx() ) );
334 
335  // As we block any signals, possible depending widgets will not being updated
336  // so we force emit signal once and for all
338 
339  // A bit of logic to set the default value if AllowNull is false and this is a new feature
340  // Note that this needs to be here after the cache has been created/updated by populate()
341  // and signals unblocked (we want this to propagate to the feature itself)
342  if ( context().attributeFormMode() != QgsAttributeEditorContext::Mode::MultiEditMode
343  && ! formFeature().attribute( fieldIdx() ).isValid()
344  && ! mCache.isEmpty()
345  && ! config( QStringLiteral( "AllowNull" ) ).toBool( ) )
346  {
347  // This is deferred because at the time the feature is set in one widget it is not
348  // set in the next, which is typically the "down" in a drill-down
349  QTimer::singleShot( 0, this, [ this ]
350  {
351  if ( ! mCache.isEmpty() )
352  {
353  updateValues( formFeature().attribute( fieldIdx() ).isValid() ? formFeature().attribute( fieldIdx() ) : mCache.at( 0 ).key );
354  }
355  } );
356  }
357 }
358 
359 int QgsValueRelationWidgetWrapper::columnCount() const
360 {
361  return std::max( 1, config( QStringLiteral( "NofColumns" ) ).toInt() );
362 }
363 
364 
365 QVariant::Type QgsValueRelationWidgetWrapper::fkType() const
366 {
368  if ( layer )
369  {
370  QgsFields fields = layer->fields();
371  int idx { fields.lookupField( config().value( QStringLiteral( "Key" ) ).toString() ) };
372  if ( idx >= 0 )
373  {
374  return fields.at( idx ).type();
375  }
376  }
377  return QVariant::Type::Invalid;
378 }
379 
380 void QgsValueRelationWidgetWrapper::populate( )
381 {
382  // Initialize, note that signals are blocked, to avoid double signals on new features
385  {
386  if ( context().parentFormFeature().isValid() )
387  {
388  mCache = QgsValueRelationFieldFormatter::createCache( config(), formFeature(), context().parentFormFeature() );
389  }
390  else
391  {
393  }
394  }
395  else if ( mCache.empty() )
396  {
398  }
399 
400  if ( mComboBox )
401  {
402  mComboBox->clear();
403  if ( config( QStringLiteral( "AllowNull" ) ).toBool( ) )
404  {
405  whileBlocking( mComboBox )->addItem( tr( "(no selection)" ), QVariant( field().type( ) ) );
406  }
407 
408  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : std::as_const( mCache ) )
409  {
410  whileBlocking( mComboBox )->addItem( element.value, element.key );
411  if ( !element.description.isEmpty() )
412  mComboBox->setItemData( mComboBox->count() - 1, element.description, Qt::ToolTipRole );
413  }
414 
415  }
416  else if ( mTableWidget )
417  {
418  const int nofColumns = columnCount();
419 
420  if ( ! mCache.empty() )
421  {
422  mTableWidget->setRowCount( ( mCache.size() + nofColumns - 1 ) / nofColumns );
423  }
424  else
425  mTableWidget->setRowCount( 1 );
426  mTableWidget->setColumnCount( nofColumns );
427 
428  whileBlocking( mTableWidget )->clear();
429  int row = 0;
430  int column = 0;
431  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : std::as_const( mCache ) )
432  {
433  if ( column == nofColumns )
434  {
435  row++;
436  column = 0;
437  }
438  QTableWidgetItem *item = nullptr;
439  item = new QTableWidgetItem( element.value );
440  item->setData( Qt::UserRole, element.key );
441  whileBlocking( mTableWidget )->setItem( row, column, item );
442  column++;
443  }
444 
445  }
446  else if ( mLineEdit )
447  {
448  QStringList values;
449  values.reserve( mCache.size() );
450  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : std::as_const( mCache ) )
451  {
452  values << i.value;
453  }
454  QStringListModel *m = new QStringListModel( values, mLineEdit );
455  QCompleter *completer = new QCompleter( m, mLineEdit );
456  completer->setCaseSensitivity( Qt::CaseInsensitive );
457  mLineEdit->setCompleter( completer );
458  }
459 }
460 
462 {
463  const int nofColumns = columnCount();
464 
465  if ( mTableWidget )
466  {
467  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
468  {
469  for ( int i = 0; i < nofColumns; ++i )
470  {
471  whileBlocking( mTableWidget )->item( j, i )->setCheckState( Qt::PartiallyChecked );
472  }
473  }
474  }
475  else if ( mComboBox )
476  {
477  whileBlocking( mComboBox )->setCurrentIndex( -1 );
478  }
479  else if ( mLineEdit )
480  {
481  whileBlocking( mLineEdit )->clear();
482  }
483 }
484 
486 {
487  if ( mEnabled == enabled )
488  return;
489 
490  mEnabled = enabled;
491 
492  if ( mTableWidget )
493  {
494  auto signalBlockedTableWidget = whileBlocking( mTableWidget );
495  Q_UNUSED( signalBlockedTableWidget )
496 
497  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
498  {
499  for ( int i = 0; i < mTableWidget->columnCount(); ++i )
500  {
501  QTableWidgetItem *item = mTableWidget->item( j, i );
502  if ( item )
503  {
504  item->setFlags( enabled ? item->flags() | Qt::ItemIsEnabled : item->flags() & ~Qt::ItemIsEnabled );
505  }
506  }
507  }
508  }
509  else
511 }
512 
513 void QgsValueRelationWidgetWrapper::parentFormValueChanged( const QString &attribute, const QVariant &value )
514 {
515 
516  // Update the parent feature in the context ( which means to replace the whole context :/ )
518  QgsFeature feature { context().parentFormFeature() };
519  feature.setAttribute( attribute, value );
520  ctx.setParentFormFeature( feature );
521  setContext( ctx );
522 
523  // Check if the change might affect the filter expression and the cache needs updates
525  && ( config( QStringLiteral( "Value" ) ).toString() == attribute ||
526  config( QStringLiteral( "Key" ) ).toString() == attribute ||
528  QgsValueRelationFieldFormatter::expressionParentFormAttributes( mExpression ).contains( attribute ) ) )
529  {
530  populate();
531  }
532 
533 }
534 
535 void QgsValueRelationWidgetWrapper::emitValueChangedInternal( const QString &value )
536 {
538  emit valueChanged( value );
540  emit valuesChanged( value );
541 }
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 unique ID, geometry and a list of field...
Definition: qgsfeature.h:56
bool setAttribute(int field, const QVariant &attr)
Sets an attribute's value by field index.
Definition: qgsfeature.cpp:255
QVariant attribute(const QString &name) const
Lookup attribute value by attribute name.
Definition: qgsfeature.cpp:320
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
Returns the 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:349
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:470
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:266
#define Q_NOWARN_DEPRECATED_POP
Definition: qgis.h:2065
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:2064
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition: qgis.h:1517