QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
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"
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>
41using namespace nlohmann;
42
43
44QgsValueRelationWidgetWrapper::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 ( QgsVariantUtils::isNull( v ) )
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
193void 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)
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
291void 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
359int QgsValueRelationWidgetWrapper::columnCount() const
360{
361 return std::max( 1, config( QStringLiteral( "NofColumns" ) ).toInt() );
362}
363
364
365QVariant::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
380void 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
513void 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
535void 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:265
QVariant attribute(const QString &name) const
Lookup attribute value by attribute name.
Definition: qgsfeature.cpp:338
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:477
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.
static bool isNull(const QVariant &variant)
Returns true if the specified variant should be considered a NULL value.
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:3061
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:3060
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition: qgis.h:2453