QGIS API Documentation  3.10.0-A Coruña (6c816b4204)
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 
38 #include <nlohmann/json.hpp>
39 using namespace nlohmann;
40 
41 
42 QgsValueRelationWidgetWrapper::QgsValueRelationWidgetWrapper( QgsVectorLayer *layer, int fieldIdx, QWidget *editor, QWidget *parent )
43  : QgsEditorWidgetWrapper( layer, fieldIdx, editor, parent )
44 {
45 }
46 
47 
49 {
50  QVariant v;
51 
52  if ( mComboBox )
53  {
54  int cbxIdx = mComboBox->currentIndex();
55  if ( cbxIdx > -1 )
56  {
57  v = mComboBox->currentData();
58  }
59  }
60 
61  const int nofColumns = columnCount();
62 
63  if ( mTableWidget )
64  {
65  QStringList selection;
66  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
67  {
68  for ( int i = 0; i < nofColumns; ++i )
69  {
70  QTableWidgetItem *item = mTableWidget->item( j, i );
71  if ( item )
72  {
73  if ( item->checkState() == Qt::Checked )
74  selection << item->data( Qt::UserRole ).toString();
75  }
76  }
77  }
78 
79  QVariantList vl;
80  //store as QVariantList because the field type supports data structure
81  for ( const QString &s : qgis::as_const( selection ) )
82  {
83  // Convert to proper type
84  const QVariant::Type type { fkType() };
85  switch ( type )
86  {
87  case QVariant::Type::Int:
88  vl.push_back( s.toInt() );
89  break;
90  case QVariant::Type::LongLong:
91  vl.push_back( s.toLongLong() );
92  break;
93  default:
94  vl.push_back( s );
95  break;
96  }
97  }
98 
99  if ( layer()->fields().at( fieldIdx() ).type() == QVariant::Map ||
100  layer()->fields().at( fieldIdx() ).type() == QVariant::List )
101  {
102  v = vl;
103  }
104  else
105  {
106  //make string
108  }
109  }
110 
111  if ( mLineEdit )
112  {
113  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &item : qgis::as_const( mCache ) )
114  {
115  if ( item.value == mLineEdit->text() )
116  {
117  v = item.key;
118  break;
119  }
120  }
121  }
122 
123  return v;
124 }
125 
127 {
128  QgsAttributeForm *form = qobject_cast<QgsAttributeForm *>( parent );
129  if ( form )
131 
132  mExpression = config().value( QStringLiteral( "FilterExpression" ) ).toString();
133 
134  if ( config( QStringLiteral( "AllowMulti" ) ).toBool() )
135  {
136  return new QTableWidget( parent );
137  }
138  else if ( config( QStringLiteral( "UseCompleter" ) ).toBool() )
139  {
140  return new QgsFilterLineEdit( parent );
141  }
142  {
143  return new QComboBox( parent );
144  }
145 }
146 
148 {
149 
150  mComboBox = qobject_cast<QComboBox *>( editor );
151  mTableWidget = qobject_cast<QTableWidget *>( editor );
152  mLineEdit = qobject_cast<QLineEdit *>( editor );
153 
154  // Read current initial form values from the editor context
156 
157  if ( mComboBox )
158  {
159  connect( mComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ),
160  this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection );
161  }
162  else if ( mTableWidget )
163  {
164  mTableWidget->horizontalHeader()->setResizeMode( QHeaderView::Stretch );
165  mTableWidget->horizontalHeader()->setVisible( false );
166  mTableWidget->verticalHeader()->setResizeMode( QHeaderView::Stretch );
167  mTableWidget->verticalHeader()->setVisible( false );
168  mTableWidget->setShowGrid( false );
169  mTableWidget->setEditTriggers( QAbstractItemView::NoEditTriggers );
170  mTableWidget->setSelectionMode( QAbstractItemView::NoSelection );
171  connect( mTableWidget, &QTableWidget::itemChanged, this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection );
172  }
173  else if ( mLineEdit )
174  {
175  connect( mLineEdit, &QLineEdit::textChanged, this, [ = ]( const QString & value )
176  {
178  emit valueChanged( value );
180  emit valuesChanged( value );
181  }, Qt::UniqueConnection );
182  }
183 }
184 
186 {
187  return mTableWidget || mLineEdit || mComboBox;
188 }
189 
190 void QgsValueRelationWidgetWrapper::updateValues( const QVariant &value, const QVariantList & )
191 {
192  if ( mTableWidget )
193  {
194  QStringList checkList;
195 
196  if ( layer()->fields().at( fieldIdx() ).type() == QVariant::Map ||
197  layer()->fields().at( fieldIdx() ).type() == QVariant::List )
198  {
199  checkList = value.toStringList();
200  }
201  else
202  {
204  }
205 
206  QTableWidgetItem *lastChangedItem = nullptr;
207 
208  const int nofColumns = columnCount();
209 
210  // This block is needed because item->setCheckState triggers dataChanged gets back to value()
211  // and iterate over all items again! This can be extremely slow on large items sets.
212  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
213  {
214  auto signalBlockedTableWidget = whileBlocking( mTableWidget );
215  Q_UNUSED( signalBlockedTableWidget )
216 
217  for ( int i = 0; i < nofColumns; ++i )
218  {
219  QTableWidgetItem *item = mTableWidget->item( j, i );
220  if ( item )
221  {
222  item->setCheckState( checkList.contains( item->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked );
223  //re-set enabled state because it's lost after reloading items
224  item->setFlags( mEnabled ? item->flags() | Qt::ItemIsEnabled : item->flags() & ~Qt::ItemIsEnabled );
225  lastChangedItem = item;
226  }
227  }
228  }
229  // let's trigger the signal now, once and for all
230  if ( lastChangedItem )
231  lastChangedItem->setCheckState( checkList.contains( lastChangedItem->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked );
232 
233  }
234  else if ( mComboBox )
235  {
236  // findData fails to tell a 0 from a NULL
237  // See: "Value relation, value 0 = NULL" - https://github.com/qgis/QGIS/issues/27803
238  int idx = -1; // default to not found
239  for ( int i = 0; i < mComboBox->count(); i++ )
240  {
241  QVariant v( mComboBox->itemData( i ) );
242  if ( qgsVariantEqual( v, value ) )
243  {
244  idx = i;
245  break;
246  }
247  }
248  mComboBox->setCurrentIndex( idx );
249  }
250  else if ( mLineEdit )
251  {
252  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : qgis::as_const( mCache ) )
253  {
254  if ( i.key == value )
255  {
256  mLineEdit->setText( i.value );
257  break;
258  }
259  }
260  }
261 }
262 
263 void QgsValueRelationWidgetWrapper::widgetValueChanged( const QString &attribute, const QVariant &newValue, bool attributeChanged )
264 {
265 
266  // Do nothing if the value has not changed
267  if ( attributeChanged )
268  {
269  QVariant oldValue( value( ) );
270  setFormFeatureAttribute( attribute, newValue );
271  // Update combos if the value used in the filter expression has changed
273  && QgsValueRelationFieldFormatter::expressionFormAttributes( mExpression ).contains( attribute ) )
274  {
275  populate();
276  // Restore value
277  updateValues( value( ) );
278  // If the value has changed as a result of another widget's value change,
279  // we need to emit the signal to make sure other dependent widgets are
280  // updated.
281  if ( oldValue != value() && fieldIdx() < formFeature().fields().count() )
282  {
283  QString attributeName( formFeature().fields().names().at( fieldIdx() ) );
284  setFormFeatureAttribute( attributeName, value( ) );
285  emitValueChanged( );
286  }
287  }
288  }
289 }
290 
291 
293 {
294  setFormFeature( feature );
295  whileBlocking( this )->populate();
296  whileBlocking( this )->setValue( feature.attribute( fieldIdx() ) );
297 
298  // As we block any signals, possible depending widgets will not being updated
299  // so we force emit signal once and for all
301 
302  // A bit of logic to set the default value if AllowNull is false and this is a new feature
303  // Note that this needs to be here after the cache has been created/updated by populate()
304  // and signals unblocked (we want this to propagate to the feature itself)
305  if ( formFeature().isValid()
306  && ! formFeature().attribute( fieldIdx() ).isValid()
307  && ! mCache.empty()
308  && ! config( QStringLiteral( "AllowNull" ) ).toBool( ) )
309  {
310  // This is deferred because at the time the feature is set in one widget it is not
311  // set in the next, which is typically the "down" in a drill-down
312  QTimer::singleShot( 0, this, [ this ]
313  {
314  updateValues( mCache.at( 0 ).key );
315  } );
316  }
317 }
318 
319 int QgsValueRelationWidgetWrapper::columnCount() const
320 {
321  return std::max( 1, config( QStringLiteral( "NofColumns" ) ).toInt() );
322 }
323 
324 
325 QVariant::Type QgsValueRelationWidgetWrapper::fkType() const
326 {
328  if ( layer )
329  {
330  QgsFields fields = layer->fields();
331  int idx { fields.lookupField( config().value( QStringLiteral( "Key" ) ).toString() ) };
332  if ( idx >= 0 )
333  {
334  return fields.at( idx ).type();
335  }
336  }
337  return QVariant::Type::Invalid;
338 }
339 
340 void QgsValueRelationWidgetWrapper::populate( )
341 {
342  // Initialize, note that signals are blocked, to avoid double signals on new features
344  {
346  }
347  else if ( mCache.empty() )
348  {
350  }
351 
352  if ( mComboBox )
353  {
354  mComboBox->clear();
355  if ( config( QStringLiteral( "AllowNull" ) ).toBool( ) )
356  {
357  whileBlocking( mComboBox )->addItem( tr( "(no selection)" ), QVariant( field().type( ) ) );
358  }
359 
360  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : qgis::as_const( mCache ) )
361  {
362  whileBlocking( mComboBox )->addItem( element.value, element.key );
363  }
364  }
365  else if ( mTableWidget )
366  {
367  const int nofColumns = columnCount();
368 
369  if ( ! mCache.empty() )
370  {
371  mTableWidget->setRowCount( ( mCache.size() + nofColumns - 1 ) / nofColumns );
372  }
373  else
374  mTableWidget->setRowCount( 1 );
375  mTableWidget->setColumnCount( nofColumns );
376 
377  whileBlocking( mTableWidget )->clear();
378  int row = 0;
379  int column = 0;
380  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : qgis::as_const( mCache ) )
381  {
382  if ( column == nofColumns )
383  {
384  row++;
385  column = 0;
386  }
387  QTableWidgetItem *item = nullptr;
388  item = new QTableWidgetItem( element.value );
389  item->setData( Qt::UserRole, element.key );
390  whileBlocking( mTableWidget )->setItem( row, column, item );
391  column++;
392  }
393  }
394  else if ( mLineEdit )
395  {
396  QStringList values;
397  values.reserve( mCache.size() );
398  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : qgis::as_const( mCache ) )
399  {
400  values << i.value;
401  }
402  QStringListModel *m = new QStringListModel( values, mLineEdit );
403  QCompleter *completer = new QCompleter( m, mLineEdit );
404  completer->setCaseSensitivity( Qt::CaseInsensitive );
405  mLineEdit->setCompleter( completer );
406  }
407 }
408 
410 {
411  const int nofColumns = columnCount();
412 
413  if ( mTableWidget )
414  {
415  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
416  {
417  for ( int i = 0; i < nofColumns; ++i )
418  {
419  whileBlocking( mTableWidget )->item( j, i )->setCheckState( Qt::PartiallyChecked );
420  }
421  }
422  }
423  else if ( mComboBox )
424  {
425  whileBlocking( mComboBox )->setCurrentIndex( -1 );
426  }
427  else if ( mLineEdit )
428  {
429  whileBlocking( mLineEdit )->clear();
430  }
431 }
432 
434 {
435  if ( mEnabled == enabled )
436  return;
437 
438  mEnabled = enabled;
439 
440  if ( mTableWidget )
441  {
442  auto signalBlockedTableWidget = whileBlocking( mTableWidget );
443  Q_UNUSED( signalBlockedTableWidget )
444 
445  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
446  {
447  for ( int i = 0; i < mTableWidget->columnCount(); ++i )
448  {
449  QTableWidgetItem *item = mTableWidget->item( j, i );
450  if ( item )
451  {
452  item->setFlags( enabled ? item->flags() | Qt::ItemIsEnabled : item->flags() & ~Qt::ItemIsEnabled );
453  }
454  }
455  }
456  }
457  else
459 }
int lookupField(const QString &fieldName) const
Looks up field&#39;s index from the field name.
Definition: qgsfields.cpp:324
void emitValueChanged()
Will call the value() method to determine the emitted value.
QgsField field() const
Access the field.
void setFeature(const QgsFeature &feature) override
Will be called when the feature changes.
void showIndeterminateState() override
Sets the widget to display in an indeterminate "mixed value" state.
Manages an editor widget Widget and wrapper share the same parent.
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:649
void setFormFeature(const QgsFeature &feature)
Set the feature currently being edited to feature.
Container of fields for a vector layer.
Definition: qgsfields.h:42
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.
QVariantMap config() const
Returns the whole config.
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:55
bool valid() const override
Returns true if the widget has been properly initialized.
QgsField at(int i) const
Gets field at particular index (must be in range 0..N-1)
Definition: qgsfields.cpp:163
QgsFields fields() const FINAL
Returns the list of fields of this layer.
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.
void initWidget(QWidget *editor) override
This method should initialize the editor widget with runtime data.
QLineEdit subclass with built in support for clearing the widget&#39;s value and handling custom null val...
static QgsVectorLayer * resolveLayer(const QVariantMap &config, const QgsProject *project)
Returns the (possibly NULL) layer from the widget&#39;s config and project.
QgsFeature formFeature() const
The feature currently being edited, in its current state.
bool setFormFeatureAttribute(const QString &attributeName, const QVariant &attributeValue)
Update the feature currently being edited by changing its attribute attributeName to attributeValue...
void widgetValueChanged(const QString &attribute, const QVariant &value, bool attributeChanged)
Notifies about changes of attributes.
QVariant createCache(QgsVectorLayer *layer, int fieldIndex, const QVariantMap &config) const override
Create a cache for a given field.
static QString buildArray(const QVariantList &list)
Build a postgres array like formatted list in a string from a QVariantList.
static QSet< QString > expressionFormAttributes(const QString &expression)
Returns a list of attributes required by the form context expression.
const QgsAttributeEditorContext & context() const
Returns information about the context in which this widget is shown.
void setEnabled(bool enabled) override
Is used to enable or disable the edit functionality of the managed widget.
#define Q_NOWARN_DEPRECATED_POP
Definition: qgis.h:650
QgsValueRelationWidgetWrapper(QgsVectorLayer *layer, int fieldIdx, QWidget *editor=nullptr, QWidget *parent=nullptr)
Constructor for QgsValueRelationWidgetWrapper.
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition: qgis.h:227
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 QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:442
static bool expressionRequiresFormScope(const QString &expression)
Check if the expression requires a form scope (i.e.
QWidget * createWidget(QWidget *parent) override
This method should create a new widget with the provided parent.
int fieldIdx() const
Access the field index.
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:298
QgsVectorLayer * layer() const
Returns the vector layer associated with the widget.
Represents a vector layer which manages a vector based data sets.
QVariant attribute(const QString &name) const
Lookup attribute value from attribute name.
Definition: qgsfeature.cpp:262
Q_DECL_DEPRECATED void valueChanged(const QVariant &value)
Emit this signal, whenever the value changed.
QVariant::Type type
Definition: qgsfield.h:56
QVariant value() const override
Will be used to access the widget&#39;s value.