QGIS API Documentation 3.28.14-Firenze (exported)
Loading...
Searching...
No Matches
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 QComboBox *combo = new QComboBox( parent );
152 combo->setMinimumContentsLength( 1 );
153 combo->setSizeAdjustPolicy( QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon );
154 return combo;
155 }
156}
157
159{
160
161 mComboBox = qobject_cast<QComboBox *>( editor );
162 mTableWidget = qobject_cast<QTableWidget *>( editor );
163 mLineEdit = qobject_cast<QLineEdit *>( editor );
164
165 // Read current initial form values from the editor context
167
168 if ( mComboBox )
169 {
170 mComboBox->view()->setVerticalScrollBarPolicy( Qt::ScrollBarAsNeeded );
171 connect( mComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ),
172 this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection );
173 }
174 else if ( mTableWidget )
175 {
176 mTableWidget->horizontalHeader()->setSectionResizeMode( QHeaderView::Stretch );
177 mTableWidget->horizontalHeader()->setVisible( false );
178 mTableWidget->verticalHeader()->setSectionResizeMode( QHeaderView::Stretch );
179 mTableWidget->verticalHeader()->setVisible( false );
180 mTableWidget->setShowGrid( false );
181 mTableWidget->setEditTriggers( QAbstractItemView::NoEditTriggers );
182 mTableWidget->setSelectionMode( QAbstractItemView::NoSelection );
183 connect( mTableWidget, &QTableWidget::itemChanged, this, static_cast<void ( QgsEditorWidgetWrapper::* )()>( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection );
184 }
185 else if ( mLineEdit )
186 {
187 connect( mLineEdit, &QLineEdit::textChanged, this, &QgsValueRelationWidgetWrapper::emitValueChangedInternal, Qt::UniqueConnection );
188 }
189}
190
192{
193 return mTableWidget || mLineEdit || mComboBox;
194}
195
196void QgsValueRelationWidgetWrapper::updateValues( const QVariant &value, const QVariantList & )
197{
198 if ( mTableWidget )
199 {
200 QStringList checkList;
201
202 if ( layer()->fields().at( fieldIdx() ).type() == QVariant::Map ||
203 layer()->fields().at( fieldIdx() ).type() == QVariant::List )
204 {
205 checkList = value.toStringList();
206 }
207 else
208 {
210 }
211
212 QTableWidgetItem *lastChangedItem = nullptr;
213
214 const int nofColumns = columnCount();
215
216 // This block is needed because item->setCheckState triggers dataChanged gets back to value()
217 // and iterate over all items again! This can be extremely slow on large items sets.
218 for ( int j = 0; j < mTableWidget->rowCount(); j++ )
219 {
220 auto signalBlockedTableWidget = whileBlocking( mTableWidget );
221 Q_UNUSED( signalBlockedTableWidget )
222
223 for ( int i = 0; i < nofColumns; ++i )
224 {
225 QTableWidgetItem *item = mTableWidget->item( j, i );
226 if ( item )
227 {
228 item->setCheckState( checkList.contains( item->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked );
229 //re-set enabled state because it's lost after reloading items
230 item->setFlags( mEnabled ? item->flags() | Qt::ItemIsEnabled : item->flags() & ~Qt::ItemIsEnabled );
231 lastChangedItem = item;
232 }
233 }
234 }
235 // let's trigger the signal now, once and for all
236 if ( lastChangedItem )
237 lastChangedItem->setCheckState( checkList.contains( lastChangedItem->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked );
238
239 }
240 else if ( mComboBox )
241 {
242 // findData fails to tell a 0 from a NULL
243 // See: "Value relation, value 0 = NULL" - https://github.com/qgis/QGIS/issues/27803
244 int idx = -1; // default to not found
245 for ( int i = 0; i < mComboBox->count(); i++ )
246 {
247 QVariant v( mComboBox->itemData( i ) );
248 if ( qgsVariantEqual( v, value ) )
249 {
250 idx = i;
251 break;
252 }
253 }
254
255 if ( idx == -1 )
256 {
257 // if value doesn't exist, we show it in '(...)' (just like value map widget)
259 {
260 mComboBox->setCurrentIndex( -1 );
261 }
262 else
263 {
264 mComboBox->addItem( value.toString().prepend( '(' ).append( ')' ), value );
265 mComboBox->setCurrentIndex( mComboBox->findData( value ) );
266 }
267 }
268 else
269 {
270 mComboBox->setCurrentIndex( idx );
271 }
272 }
273 else if ( mLineEdit )
274 {
275 mLineEdit->clear();
276 bool wasFound { false };
277 for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : std::as_const( mCache ) )
278 {
279 if ( i.key == value )
280 {
281 mLineEdit->setText( i.value );
282 wasFound = true;
283 break;
284 }
285 }
286 // Value could not be found
287 if ( ! wasFound )
288 {
289 mLineEdit->setText( tr( "(no selection)" ) );
290 }
291 }
292}
293
294void QgsValueRelationWidgetWrapper::widgetValueChanged( const QString &attribute, const QVariant &newValue, bool attributeChanged )
295{
296
297 // Do nothing if the value has not changed
298 if ( attributeChanged )
299 {
300 QVariant oldValue( value( ) );
301 setFormFeatureAttribute( attribute, newValue );
302 // Update combos if the value used in the filter expression has changed
304 && QgsValueRelationFieldFormatter::expressionFormAttributes( mExpression ).contains( attribute ) )
305 {
306 populate();
307 // Restore value
308 updateValues( value( ) );
309 // If the value has changed as a result of another widget's value change,
310 // we need to emit the signal to make sure other dependent widgets are
311 // updated.
312 QgsFields formFields( formFeature().fields() );
313
314 // Also check for fields in the layer in case this is a multi-edit form
315 // and there is not form feature set
316 if ( formFields.count() == 0 && layer() )
317 {
318 formFields = layer()->fields();
319 }
320
321 if ( oldValue != value() && fieldIdx() < formFields.count() )
322 {
323 QString attributeName( formFields.names().at( fieldIdx() ) );
324 setFormFeatureAttribute( attributeName, value( ) );
326 }
327 }
328 }
329}
330
331
333{
334 setFormFeature( feature );
335 whileBlocking( this )->populate();
336 whileBlocking( this )->setValue( feature.attribute( fieldIdx() ) );
337
338 // As we block any signals, possible depending widgets will not being updated
339 // so we force emit signal once and for all
341
342 // A bit of logic to set the default value if AllowNull is false and this is a new feature
343 // Note that this needs to be here after the cache has been created/updated by populate()
344 // and signals unblocked (we want this to propagate to the feature itself)
345 if ( context().attributeFormMode() != QgsAttributeEditorContext::Mode::MultiEditMode
346 && ! formFeature().attribute( fieldIdx() ).isValid()
347 && ! mCache.isEmpty()
348 && ! config( QStringLiteral( "AllowNull" ) ).toBool( ) )
349 {
350 // This is deferred because at the time the feature is set in one widget it is not
351 // set in the next, which is typically the "down" in a drill-down
352 QTimer::singleShot( 0, this, [ this ]
353 {
354 if ( ! mCache.isEmpty() )
355 {
356 updateValues( formFeature().attribute( fieldIdx() ).isValid() ? formFeature().attribute( fieldIdx() ) : mCache.at( 0 ).key );
357 }
358 } );
359 }
360}
361
362int QgsValueRelationWidgetWrapper::columnCount() const
363{
364 return std::max( 1, config( QStringLiteral( "NofColumns" ) ).toInt() );
365}
366
367
368QVariant::Type QgsValueRelationWidgetWrapper::fkType() const
369{
371 if ( layer )
372 {
373 QgsFields fields = layer->fields();
374 int idx { fields.lookupField( config().value( QStringLiteral( "Key" ) ).toString() ) };
375 if ( idx >= 0 )
376 {
377 return fields.at( idx ).type();
378 }
379 }
380 return QVariant::Type::Invalid;
381}
382
383void QgsValueRelationWidgetWrapper::populate( )
384{
385 // Initialize, note that signals are blocked, to avoid double signals on new features
388 {
389 if ( context().parentFormFeature().isValid() )
390 {
391 mCache = QgsValueRelationFieldFormatter::createCache( config(), formFeature(), context().parentFormFeature() );
392 }
393 else
394 {
396 }
397 }
398 else if ( mCache.empty() )
399 {
401 }
402
403 if ( mComboBox )
404 {
405 mComboBox->clear();
406 if ( config( QStringLiteral( "AllowNull" ) ).toBool( ) )
407 {
408 whileBlocking( mComboBox )->addItem( tr( "(no selection)" ), QVariant( field().type( ) ) );
409 }
410
411 for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : std::as_const( mCache ) )
412 {
413 whileBlocking( mComboBox )->addItem( element.value, element.key );
414 if ( !element.description.isEmpty() )
415 mComboBox->setItemData( mComboBox->count() - 1, element.description, Qt::ToolTipRole );
416 }
417
418 }
419 else if ( mTableWidget )
420 {
421 const int nofColumns = columnCount();
422
423 if ( ! mCache.empty() )
424 {
425 mTableWidget->setRowCount( ( mCache.size() + nofColumns - 1 ) / nofColumns );
426 }
427 else
428 mTableWidget->setRowCount( 1 );
429 mTableWidget->setColumnCount( nofColumns );
430
431 whileBlocking( mTableWidget )->clear();
432 int row = 0;
433 int column = 0;
434 for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : std::as_const( mCache ) )
435 {
436 if ( column == nofColumns )
437 {
438 row++;
439 column = 0;
440 }
441 QTableWidgetItem *item = nullptr;
442 item = new QTableWidgetItem( element.value );
443 item->setData( Qt::UserRole, element.key );
444 whileBlocking( mTableWidget )->setItem( row, column, item );
445 column++;
446 }
447
448 }
449 else if ( mLineEdit )
450 {
451 QStringList values;
452 values.reserve( mCache.size() );
453 for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : std::as_const( mCache ) )
454 {
455 values << i.value;
456 }
457 QStringListModel *m = new QStringListModel( values, mLineEdit );
458 QCompleter *completer = new QCompleter( m, mLineEdit );
459 completer->setCaseSensitivity( Qt::CaseInsensitive );
460 mLineEdit->setCompleter( completer );
461 }
462}
463
465{
466 const int nofColumns = columnCount();
467
468 if ( mTableWidget )
469 {
470 for ( int j = 0; j < mTableWidget->rowCount(); j++ )
471 {
472 for ( int i = 0; i < nofColumns; ++i )
473 {
474 whileBlocking( mTableWidget )->item( j, i )->setCheckState( Qt::PartiallyChecked );
475 }
476 }
477 }
478 else if ( mComboBox )
479 {
480 whileBlocking( mComboBox )->setCurrentIndex( -1 );
481 }
482 else if ( mLineEdit )
483 {
484 whileBlocking( mLineEdit )->clear();
485 }
486}
487
489{
490 if ( mEnabled == enabled )
491 return;
492
493 mEnabled = enabled;
494
495 if ( mTableWidget )
496 {
497 auto signalBlockedTableWidget = whileBlocking( mTableWidget );
498 Q_UNUSED( signalBlockedTableWidget )
499
500 for ( int j = 0; j < mTableWidget->rowCount(); j++ )
501 {
502 for ( int i = 0; i < mTableWidget->columnCount(); ++i )
503 {
504 QTableWidgetItem *item = mTableWidget->item( j, i );
505 if ( item )
506 {
507 item->setFlags( enabled ? item->flags() | Qt::ItemIsEnabled : item->flags() & ~Qt::ItemIsEnabled );
508 }
509 }
510 }
511 }
512 else
514}
515
516void QgsValueRelationWidgetWrapper::parentFormValueChanged( const QString &attribute, const QVariant &value )
517{
518
519 // Update the parent feature in the context ( which means to replace the whole context :/ )
521 QgsFeature feature { context().parentFormFeature() };
522 feature.setAttribute( attribute, value );
523 ctx.setParentFormFeature( feature );
524 setContext( ctx );
525
526 // Check if the change might affect the filter expression and the cache needs updates
528 && ( config( QStringLiteral( "Value" ) ).toString() == attribute ||
529 config( QStringLiteral( "Key" ) ).toString() == attribute ||
531 QgsValueRelationFieldFormatter::expressionParentFormAttributes( mExpression ).contains( attribute ) ) )
532 {
533 populate();
534 }
535
536}
537
538void QgsValueRelationWidgetWrapper::emitValueChangedInternal( const QString &value )
539{
541 emit valueChanged( value );
543 emit valuesChanged( value );
544}
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.
@ MultiEditMode
Multi edit mode, for editing fields of multiple features at once.
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.
QVariant attribute(const QString &name) const
Lookup attribute value by attribute name.
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.
QgsField at(int i) const
Returns the field at particular index (must be in range 0..N-1).
int lookupField(const QString &fieldName) const
Looks up field's index from the field name.
QStringList names() const
Returns a list with field names.
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.
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