QGIS API Documentation  3.8.0-Zanzibar (11aff65)
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 
30 #include <QHeaderView>
31 #include <QComboBox>
32 #include <QLineEdit>
33 #include <QTableWidget>
34 #include <QStringListModel>
35 #include <QCompleter>
36 
37 #include <nlohmann/json.hpp>
39 
40 
41 QgsValueRelationWidgetWrapper::QgsValueRelationWidgetWrapper( QgsVectorLayer *layer, int fieldIdx, QWidget *editor, QWidget *parent )
42  : QgsEditorWidgetWrapper( layer, fieldIdx, editor, parent )
43 {
44 }
45 
46 
48 {
49  QVariant v;
50 
51  if ( mComboBox )
52  {
53  int cbxIdx = mComboBox->currentIndex();
54  if ( cbxIdx > -1 )
55  {
56  v = mComboBox->currentData();
57  }
58  }
59 
60  const int nofColumns = columnCount();
61 
62  if ( mTableWidget )
63  {
64  QStringList selection;
65  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
66  {
67  for ( int i = 0; i < nofColumns; ++i )
68  {
69  QTableWidgetItem *item = mTableWidget->item( j, i );
70  if ( item )
71  {
72  if ( item->checkState() == Qt::Checked )
73  selection << item->data( Qt::UserRole ).toString();
74  }
75  }
76  }
77 
78  QVariantList vl;
79  //store as QVariantList because it's json
80  for ( const QString &s : qgis::as_const( selection ) )
81  {
82  // Convert to proper type
83  const QVariant::Type type { fkType() };
84  switch ( type )
85  {
86  case QVariant::Type::Int:
87  vl.push_back( s.toInt() );
88  break;
89  case QVariant::Type::LongLong:
90  vl.push_back( s.toLongLong() );
91  break;
92  default:
93  vl.push_back( s );
94  break;
95  }
96  }
97  v = vl;
98  }
99 
100  if ( mLineEdit )
101  {
102  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &item : qgis::as_const( mCache ) )
103  {
104  if ( item.value == mLineEdit->text() )
105  {
106  v = item.key;
107  break;
108  }
109  }
110  }
111 
112  return v;
113 }
114 
116 {
117  QgsAttributeForm *form = qobject_cast<QgsAttributeForm *>( parent );
118  if ( form )
120 
121  mExpression = config().value( QStringLiteral( "FilterExpression" ) ).toString();
122 
123  if ( config( QStringLiteral( "AllowMulti" ) ).toBool() )
124  {
125  return new QTableWidget( parent );
126  }
127  else if ( config( QStringLiteral( "UseCompleter" ) ).toBool() )
128  {
129  return new QgsFilterLineEdit( parent );
130  }
131  {
132  return new QComboBox( parent );
133  }
134 }
135 
137 {
138 
139  mComboBox = qobject_cast<QComboBox *>( editor );
140  mTableWidget = qobject_cast<QTableWidget *>( editor );
141  mLineEdit = qobject_cast<QLineEdit *>( editor );
142 
143  // Read current initial form values from the editor context
145 
146  if ( mComboBox )
147  {
148  connect( mComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ),
149  this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection );
150  }
151  else if ( mTableWidget )
152  {
153  mTableWidget->horizontalHeader()->setResizeMode( QHeaderView::Stretch );
154  mTableWidget->horizontalHeader()->setVisible( false );
155  mTableWidget->verticalHeader()->setResizeMode( QHeaderView::Stretch );
156  mTableWidget->verticalHeader()->setVisible( false );
157  mTableWidget->setShowGrid( false );
158  mTableWidget->setEditTriggers( QAbstractItemView::NoEditTriggers );
159  mTableWidget->setSelectionMode( QAbstractItemView::NoSelection );
160  connect( mTableWidget, &QTableWidget::itemChanged, this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection );
161  }
162  else if ( mLineEdit )
163  {
164  connect( mLineEdit, &QLineEdit::textChanged, this, [ = ]( const QString & value ) { emit valueChanged( value ); }, Qt::UniqueConnection );
165  }
166 }
167 
169 {
170  return mTableWidget || mLineEdit || mComboBox;
171 }
172 
174 {
175  if ( mTableWidget )
176  {
177  QStringList checkList;
178 
179  if ( layer()->fields().at( fieldIdx() ).type() == QVariant::Map )
180  {
181  //because of json it's stored as QVariantList
182  checkList = value.toStringList();
183  }
184  else
185  {
187  }
188 
189  QTableWidgetItem *lastChangedItem = nullptr;
190 
191  const int nofColumns = columnCount();
192 
193  // This block is needed because item->setCheckState triggers dataChanged gets back to value()
194  // and iterate over all items again! This can be extremely slow on large items sets.
195  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
196  {
197  auto signalBlockedTableWidget = whileBlocking( mTableWidget );
198  Q_UNUSED( signalBlockedTableWidget )
199 
200  for ( int i = 0; i < nofColumns; ++i )
201  {
202  QTableWidgetItem *item = mTableWidget->item( j, i );
203  if ( item )
204  {
205  item->setCheckState( checkList.contains( item->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked );
206  //re-set enabled state because it's lost after reloading items
207  item->setFlags( mEnabled ? item->flags() | Qt::ItemIsEnabled : item->flags() & ~Qt::ItemIsEnabled );
208  lastChangedItem = item;
209  }
210  }
211  }
212  // let's trigger the signal now, once and for all
213  if ( lastChangedItem )
214  lastChangedItem->setCheckState( checkList.contains( lastChangedItem->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked );
215 
216  }
217  else if ( mComboBox )
218  {
219  // findData fails to tell a 0 from a NULL
220  // See: "Value relation, value 0 = NULL" - https://github.com/qgis/QGIS/issues/27803
221  int idx = -1; // default to not found
222  for ( int i = 0; i < mComboBox->count(); i++ )
223  {
224  QVariant v( mComboBox->itemData( i ) );
225  if ( qgsVariantEqual( v, value ) )
226  {
227  idx = i;
228  break;
229  }
230  }
231  mComboBox->setCurrentIndex( idx );
232  }
233  else if ( mLineEdit )
234  {
235  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : qgis::as_const( mCache ) )
236  {
237  if ( i.key == value )
238  {
239  mLineEdit->setText( i.value );
240  break;
241  }
242  }
243  }
244 }
245 
246 void QgsValueRelationWidgetWrapper::widgetValueChanged( const QString &attribute, const QVariant &newValue, bool attributeChanged )
247 {
248 
249  // Do nothing if the value has not changed
250  if ( attributeChanged )
251  {
252  QVariant oldValue( value( ) );
253  setFormFeatureAttribute( attribute, newValue );
254  // Update combos if the value used in the filter expression has changed
256  && QgsValueRelationFieldFormatter::expressionFormAttributes( mExpression ).contains( attribute ) )
257  {
258  populate();
259  // Restore value
260  setValue( value( ) );
261  // If the value has changed as a result of another widget's value change,
262  // we need to emit the signal to make sure other dependent widgets are
263  // updated.
264  if ( oldValue != value() && fieldIdx() < formFeature().fields().count() )
265  {
266  QString attributeName( formFeature().fields().names().at( fieldIdx() ) );
267  setFormFeatureAttribute( attributeName, value( ) );
268  emitValueChanged( );
269  }
270  }
271  }
272 }
273 
274 
276 {
277  setFormFeature( feature );
278  whileBlocking( this )->populate();
279  whileBlocking( this )->setValue( feature.attribute( fieldIdx() ) );
280  // A bit of logic to set the default value if AllowNull is false and this is a new feature
281  // Note that this needs to be here after the cache has been created/updated by populate()
282  // and signals unblocked (we want this to propagate to the feature itself)
283  if ( formFeature().isValid()
284  && ! formFeature().attribute( fieldIdx() ).isValid()
285  && ! mCache.empty()
286  && ! config( QStringLiteral( "AllowNull" ) ).toBool( ) )
287  {
288  // This is deferred because at the time the feature is set in one widget it is not
289  // set in the next, which is typically the "down" in a drill-down
290  QTimer::singleShot( 0, this, [ this ]
291  {
292  setValue( mCache.at( 0 ).key );
293  } );
294  }
295 }
296 
297 int QgsValueRelationWidgetWrapper::columnCount() const
298 {
299  return std::max( 1, config( QStringLiteral( "NofColumns" ) ).toInt() );
300 }
301 
302 
303 QVariant::Type QgsValueRelationWidgetWrapper::fkType() const
304 {
306  if ( layer )
307  {
308  QgsFields fields = layer->fields();
309  int idx { fields.lookupField( config().value( QStringLiteral( "Key" ) ).toString() ) };
310  if ( idx >= 0 )
311  {
312  return fields.at( idx ).type();
313  }
314  }
315  return QVariant::Type::Invalid;
316 }
317 
318 void QgsValueRelationWidgetWrapper::populate( )
319 {
320  // Initialize, note that signals are blocked, to avoid double signals on new features
322  {
324  }
325  else if ( mCache.empty() )
326  {
328  }
329 
330  if ( mComboBox )
331  {
332  mComboBox->clear();
333  if ( config( QStringLiteral( "AllowNull" ) ).toBool( ) )
334  {
335  whileBlocking( mComboBox )->addItem( tr( "(no selection)" ), QVariant( field().type( ) ) );
336  }
337 
338  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : qgis::as_const( mCache ) )
339  {
340  whileBlocking( mComboBox )->addItem( element.value, element.key );
341  }
342  }
343  else if ( mTableWidget )
344  {
345  const int nofColumns = columnCount();
346 
347  if ( ! mCache.empty() )
348  {
349  mTableWidget->setRowCount( ( mCache.size() + nofColumns - 1 ) / nofColumns );
350  }
351  else
352  mTableWidget->setRowCount( 1 );
353  mTableWidget->setColumnCount( nofColumns );
354 
355  whileBlocking( mTableWidget )->clear();
356  int row = 0;
357  int column = 0;
358  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : qgis::as_const( mCache ) )
359  {
360  if ( column == nofColumns )
361  {
362  row++;
363  column = 0;
364  }
365  QTableWidgetItem *item = nullptr;
366  item = new QTableWidgetItem( element.value );
367  item->setData( Qt::UserRole, element.key );
368  whileBlocking( mTableWidget )->setItem( row, column, item );
369  column++;
370  }
371  }
372  else if ( mLineEdit )
373  {
374  QStringList values;
375  values.reserve( mCache.size() );
376  for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : qgis::as_const( mCache ) )
377  {
378  values << i.value;
379  }
380  QStringListModel *m = new QStringListModel( values, mLineEdit );
381  QCompleter *completer = new QCompleter( m, mLineEdit );
382  completer->setCaseSensitivity( Qt::CaseInsensitive );
383  mLineEdit->setCompleter( completer );
384  }
385 }
386 
388 {
389  const int nofColumns = columnCount();
390 
391  if ( mTableWidget )
392  {
393  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
394  {
395  for ( int i = 0; i < nofColumns; ++i )
396  {
397  whileBlocking( mTableWidget )->item( j, i )->setCheckState( Qt::PartiallyChecked );
398  }
399  }
400  }
401  else if ( mComboBox )
402  {
403  whileBlocking( mComboBox )->setCurrentIndex( -1 );
404  }
405  else if ( mLineEdit )
406  {
407  whileBlocking( mLineEdit )->clear();
408  }
409 }
410 
412 {
413  if ( mEnabled == enabled )
414  return;
415 
416  mEnabled = enabled;
417 
418  if ( mTableWidget )
419  {
420  auto signalBlockedTableWidget = whileBlocking( mTableWidget );
421  Q_UNUSED( signalBlockedTableWidget )
422 
423  for ( int j = 0; j < mTableWidget->rowCount(); j++ )
424  {
425  for ( int i = 0; i < mTableWidget->columnCount(); ++i )
426  {
427  QTableWidgetItem *item = mTableWidget->item( j, i );
428  if ( item )
429  {
430  item->setFlags( enabled ? item->flags() | Qt::ItemIsEnabled : item->flags() & ~Qt::ItemIsEnabled );
431  }
432  }
433  }
434  }
435  else
437 }
int lookupField(const QString &fieldName) const
Looks up field&#39;s index from the field name.
Definition: qgsfields.cpp:324
bool isValid() const
Returns the validity of this feature.
Definition: qgsfeature.cpp:183
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.
void setFormFeature(const QgsFeature &feature)
Set the feature currently being edited to feature.
void setValue(const QVariant &value) override
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.
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...
nlohmann::json json
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.
nlohmann::json json
Definition: qgsjsonutils.h:27
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.
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:212
void valueChanged(const QVariant &value)
Emit this signal, whenever the value changed.
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:438
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, NULL values are treated as equal...
Definition: qgis.cpp:289
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
QVariant::Type type
Definition: qgsfield.h:56
QVariant value() const override
Will be used to access the widget&#39;s value.