QGIS API Documentation 4.1.0-Master (31622b25bb0)
Loading...
Searching...
No Matches
qgsattributeform.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsattributeform.cpp
3 --------------------------------------
4 Date : 3.5.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
16#include "qgsattributeform.h"
17
19#include "qgsapplication.h"
36#include "qgsfeatureiterator.h"
37#include "qgsfeaturerequest.h"
38#include "qgsfieldmodel.h"
39#include "qgsgui.h"
41#include "qgslogger.h"
42#include "qgsmessagebar.h"
43#include "qgsmessagebaritem.h"
45#include "qgsproject.h"
46#include "qgsprojectutils.h"
47#include "qgspythonrunner.h"
48#include "qgsqmlwidgetwrapper.h"
49#include "qgsrelationmanager.h"
51#include "qgsscrollarea.h"
55#include "qgstabwidget.h"
56#include "qgstexteditwrapper.h"
61#include "qgsvectorlayerutils.h"
62
63#include <QDir>
64#include <QFile>
65#include <QFileInfo>
66#include <QFormLayout>
67#include <QGridLayout>
68#include <QKeyEvent>
69#include <QLabel>
70#include <QMenu>
71#include <QMessageBox>
72#include <QPushButton>
73#include <QString>
74#include <QSvgWidget>
75#include <QTextStream>
76#include <QToolButton>
77#include <QToolTip>
78#include <QUiLoader>
79
80#include "moc_qgsattributeform.cpp"
81
82using namespace Qt::StringLiterals;
83
84int QgsAttributeForm::sFormCounter = 0;
85
87 : QWidget( parent )
88 , mLayer( vl )
89 , mContext( context )
90 , mFormNr( sFormCounter++ )
91 , mEditCommandMessage( tr( "Attributes changed" ) )
92{
93 init();
94 initPython();
96
97 connect( vl, &QgsVectorLayer::updatedFields, this, &QgsAttributeForm::onUpdatedFields );
98 connect( vl, &QgsVectorLayer::beforeAddingExpressionField, this, &QgsAttributeForm::preventFeatureRefresh );
99 connect( vl, &QgsVectorLayer::beforeRemovingExpressionField, this, &QgsAttributeForm::preventFeatureRefresh );
100 connect( vl, &QgsVectorLayer::selectionChanged, this, &QgsAttributeForm::layerSelectionChanged );
101 connect( this, &QgsAttributeForm::modeChanged, this, &QgsAttributeForm::updateContainersVisibility );
102
103 updateContainersVisibility();
104 updateLabels();
105 updateEditableState();
106}
107
109{
110 cleanPython();
111 qDeleteAll( mInterfaces );
112}
113
115{
116 mButtonBox->hide();
117
118 // Make sure that changes are taken into account if somebody tries to figure out if there have been some
121}
122
124{
125 mButtonBox->show();
126
127 disconnect( mLayer, &QgsVectorLayer::beforeModifiedCheck, this, &QgsAttributeForm::save );
128}
129
131{
132 disconnect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsAttributeForm::save );
133 disconnect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsAttributeForm::resetValues );
134}
135
137{
138 mInterfaces.append( iface );
139}
140
142{
143 return mFeature.isValid() && mLayer->isEditable();
144}
145
147{
148 if ( mode == mMode )
149 return;
150
152 {
153 //switching out of multi edit mode triggers a save
154 if ( mUnsavedMultiEditChanges )
155 {
156 // prompt for save
157 int res = QMessageBox::question( this, tr( "Multiedit Attributes" ), tr( "Apply changes to edited features?" ), QMessageBox::Yes | QMessageBox::No );
158 if ( res == QMessageBox::Yes )
159 {
160 save();
161 }
162 }
163 clearMultiEditMessages();
164 }
165 mUnsavedMultiEditChanges = false;
166
167 mMode = mode;
168
169 if ( mButtonBox->isVisible() && mMode == QgsAttributeEditorContext::SingleEditMode )
170 {
172 }
173 else
174 {
175 disconnect( mLayer, &QgsVectorLayer::beforeModifiedCheck, this, &QgsAttributeForm::save );
176 }
177
178 //update all form editor widget modes to match
179 for ( QgsAttributeFormWidget *w : std::as_const( mFormWidgets ) )
180 {
181 switch ( mode )
182 {
185 break;
186
189 break;
190
193 break;
194
197 break;
198
201 break;
202
205 break;
206
209 break;
210
213 break;
214 }
215 }
216 //update all form editor widget modes to match
217 for ( QgsWidgetWrapper *w : std::as_const( mWidgets ) )
218 {
219 QgsAttributeEditorContext newContext = w->context();
220 newContext.setAttributeFormMode( mMode );
221 w->setContext( newContext );
222 }
223
224 auto setRelationWidgetsVisible = [this]( bool relationWidgetsVisible ) {
225 for ( QgsAttributeFormRelationEditorWidget *w : findChildren<QgsAttributeFormRelationEditorWidget *>() )
226 {
227 w->setVisible( relationWidgetsVisible );
228 }
229 };
230
231 switch ( mode )
232 {
234 setRelationWidgetsVisible( true );
235 setFeature( mFeature );
236 mSearchButtonBox->setVisible( false );
237 break;
238
240 setRelationWidgetsVisible( true );
241 synchronizeState();
242 mSearchButtonBox->setVisible( false );
243 break;
244
246 setRelationWidgetsVisible( true );
247 synchronizeState();
248 mSearchButtonBox->setVisible( false );
249 break;
250
252 setRelationWidgetsVisible( true );
253 resetMultiEdit( false );
254 synchronizeState();
255 mSearchButtonBox->setVisible( false );
256 break;
257
259 setRelationWidgetsVisible( true );
260 mSearchButtonBox->setVisible( true );
261 synchronizeState();
263 break;
264
266 setRelationWidgetsVisible( false );
267 mSearchButtonBox->setVisible( false );
268 synchronizeState();
270 break;
271
273 setRelationWidgetsVisible( true );
274 setFeature( mFeature );
275 synchronizeState();
276 mSearchButtonBox->setVisible( false );
277 break;
278
280 setRelationWidgetsVisible( false );
281 synchronizeState();
282 mSearchButtonBox->setVisible( false );
283 break;
284 }
285
286 emit modeChanged( mMode );
287}
288
289void QgsAttributeForm::changeAttribute( const QString &field, const QVariant &value, const QString &hintText )
290{
291 const auto constMWidgets = mWidgets;
292 for ( QgsWidgetWrapper *ww : constMWidgets )
293 {
294 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
295 if ( eww )
296 {
297 if ( eww->field().name() == field )
298 {
299 eww->setValues( value, QVariantList() );
300 eww->setHint( hintText );
301 }
302 // see if the field is present in additional fields of the editor widget
303 int index = eww->additionalFields().indexOf( field );
304 if ( index >= 0 )
305 {
306 QVariant mainValue = eww->value();
307 QVariantList additionalFieldValues = eww->additionalFieldValues();
308 additionalFieldValues[index] = value;
309 eww->setValues( mainValue, additionalFieldValues );
310 eww->setHint( hintText );
311 }
312 }
313 }
314}
315
317{
318 mFeature.setGeometry( geometry );
319}
320
322{
323 mIsSettingFeature = true;
324 mFeature = feature;
325 mCurrentFormFeature = feature;
326
327 switch ( mMode )
328 {
334 {
335 resetValues();
336
337 synchronizeState();
338
339 // Settings of feature is done when we trigger the attribute form interface
340 // Issue https://github.com/qgis/QGIS/issues/29667
341 mIsSettingFeature = false;
342 const auto constMInterfaces = mInterfaces;
343 for ( QgsAttributeFormInterface *iface : constMInterfaces )
344 {
345 iface->featureChanged();
346 }
347 break;
348 }
351 {
352 resetValues();
353 break;
354 }
356 {
357 //ignore setFeature
358 break;
359 }
360 }
361 mIsSettingFeature = false;
362}
363
364bool QgsAttributeForm::saveEdits( QString *error )
365{
366 bool success = true;
367 bool changedLayer = false;
368
369 QgsFeature updatedFeature = QgsFeature( mFeature );
370 if ( mFeature.isValid() || mMode == QgsAttributeEditorContext::AddFeatureMode )
371 {
372 bool doUpdate = false;
373
374 // An add dialog should perform an action by default
375 // and not only if attributes have "changed"
377 {
378 doUpdate = true;
379 }
380
381 QgsAttributes src = mFeature.attributes();
382 QgsAttributes dst = mFeature.attributes();
383
384 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
385 {
386 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
387 if ( eww )
388 {
389 // check for invalid JSON values
390 QgsTextEditWrapper *textEdit = qobject_cast<QgsTextEditWrapper *>( eww );
391 if ( textEdit && textEdit->isInvalidJSON() )
392 {
393 if ( error )
394 *error = tr( "JSON value for %1 is invalid and has not been saved" ).arg( eww->field().name() );
395 return false;
396 }
397 QVariantList dstVars = QVariantList() << dst.at( eww->fieldIdx() );
398 QVariantList srcVars = QVariantList() << eww->value();
399 QList<int> fieldIndexes = QList<int>() << eww->fieldIdx();
400
401 // append additional fields
402 const QStringList additionalFields = eww->additionalFields();
403 for ( const QString &fieldName : additionalFields )
404 {
405 int idx = eww->layer()->fields().lookupField( fieldName );
406 fieldIndexes << idx;
407 dstVars << dst.at( idx );
408 }
409 srcVars.append( eww->additionalFieldValues() );
410
411 Q_ASSERT( dstVars.count() == srcVars.count() );
412
413 for ( int i = 0; i < dstVars.count(); i++ )
414 {
415 if ( !qgsVariantEqual( dstVars[i], srcVars[i] ) && srcVars[i].isValid() )
416 {
417 dst[fieldIndexes[i]] = srcVars[i];
418
419 doUpdate = true;
420 }
421 }
422 }
423 }
424
425 updatedFeature.setAttributes( dst );
426
427 const auto constMInterfaces = mInterfaces;
428 for ( QgsAttributeFormInterface *iface : constMInterfaces )
429 {
430 if ( !iface->acceptChanges( updatedFeature ) )
431 {
432 doUpdate = false;
433 }
434 }
435
436 if ( doUpdate )
437 {
439 {
440 mFeature = updatedFeature;
441 }
443 {
444 mFeature.setValid( true );
445 mLayer->beginEditCommand( mEditCommandMessage );
446 bool res = mLayer->addFeature( updatedFeature );
447 if ( res )
448 {
449 mFeature.setAttributes( updatedFeature.attributes() );
450 mLayer->endEditCommand();
451
452 const QgsFields fields = mLayer->fields();
453 const QgsAttributes newValues = updatedFeature.attributes();
454 const QVariant lastUsedValuesVariant = mLayer->property( "AttributeFormLastUsedValues" );
455 QgsAttributeMap lastUsedValues = lastUsedValuesVariant.isValid() ? lastUsedValuesVariant.value<QgsAttributeMap>() : QgsAttributeMap();
456 for ( int idx = 0; idx < fields.count(); ++idx )
457 {
458 const Qgis::AttributeFormReuseLastValuePolicy reusePolicy = mLayer->editFormConfig().reuseLastValuePolicy( idx );
460 {
461 const QVariant rememberLastUsedValuesVariant = mLayer->property( "AttributeFormRememberLastUsedValues" );
462 QMap<int, bool> rememberLastUsedValues = rememberLastUsedValuesVariant.value<QMap<int, bool>>();
463 if ( !rememberLastUsedValues.contains( idx ) )
464 {
465 const bool remember = reusePolicy == Qgis::AttributeFormReuseLastValuePolicy::AllowedDefaultOn;
466 rememberLastUsedValues[idx] = remember;
467 mLayer->setProperty( "AttributeFormRememberLastUsedValues", QVariant::fromValue<QMap<int, bool>>( rememberLastUsedValues ) );
468 }
469
470 const QVariant newValue = rememberLastUsedValues[idx] ? newValues.at( idx ) : QVariant();
471 if ( !lastUsedValues.contains( idx ) || lastUsedValues[idx] != newValue )
472 {
473 lastUsedValues[idx] = newValue;
474 QgsDebugMsgLevel( u"Saving %1 for %2"_s.arg( ( newValue.toString() ).arg( idx ) ), 2 );
475 }
476 }
477 }
478 mLayer->setProperty( "AttributeFormLastUsedValues", QVariant::fromValue<QgsAttributeMap>( lastUsedValues ) );
479
481 changedLayer = true;
482 }
483 else
484 mLayer->destroyEditCommand();
485 }
486 else
487 {
488 mLayer->beginEditCommand( mEditCommandMessage );
489
490 QgsAttributeMap newValues;
491 QgsAttributeMap oldValues;
492
493 int n = 0;
494 for ( int i = 0; i < dst.count(); ++i )
495 {
496 if ( qgsVariantEqual( dst.at( i ), src.at( i ) ) // If field is not changed...
497 || !dst.at( i ).isValid() // or the widget returns invalid (== do not change)
498 || !fieldIsEditable( i ) ) // or the field cannot be edited ...
499 {
500 continue;
501 }
502
503 QgsDebugMsgLevel( u"Updating field %1"_s.arg( i ), 2 );
504 QgsDebugMsgLevel( u"dst:'%1' (type:%2, isNull:%3, isValid:%4)"_s.arg( dst.at( i ).toString(), dst.at( i ).typeName() ).arg( QgsVariantUtils::isNull( dst.at( i ) ) ).arg( dst.at( i ).isValid() ), 2 );
505 QgsDebugMsgLevel( u"src:'%1' (type:%2, isNull:%3, isValid:%4)"_s.arg( src.at( i ).toString(), src.at( i ).typeName() ).arg( QgsVariantUtils::isNull( src.at( i ) ) ).arg( src.at( i ).isValid() ), 2 );
506
507 newValues[i] = dst.at( i );
508 oldValues[i] = src.at( i );
509
510 n++;
511 }
512
513 auto context = std::make_unique<QgsVectorLayerToolsContext>();
514 QgsExpressionContext expressionContext = createExpressionContext( updatedFeature );
515 context->setExpressionContext( &expressionContext );
516 success = mLayer->changeAttributeValues( mFeature.id(), newValues, oldValues, false, context.get() );
517
518 if ( success && n > 0 )
519 {
520 mLayer->endEditCommand();
521 mFeature.setAttributes( dst );
522 changedLayer = true;
523 }
524 else
525 {
526 mLayer->destroyEditCommand();
527 }
528 }
529 }
530 }
531
532 emit featureSaved( updatedFeature );
533
534 // [MD] Refresh canvas only when absolutely necessary - it interferes with other stuff (#11361).
535 // This code should be revisited - and the signals should be fired (+ layer repainted)
536 // only when actually doing any changes. I am unsure if it is actually a good idea
537 // to call save() whenever some code asks for vector layer's modified status
538 // (which is the case when attribute table is open)
539 if ( changedLayer )
540 mLayer->triggerRepaint();
541
542 return success;
543}
544
545QgsFeature QgsAttributeForm::getUpdatedFeature() const
546{
547 // create updated Feature
548 QgsFeature updatedFeature = QgsFeature( mFeature );
549
550 QgsAttributes featureAttributes = mFeature.attributes();
551 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
552 {
553 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
554 if ( !eww )
555 continue;
556
557 QVariantList dstVars = QVariantList() << featureAttributes.at( eww->fieldIdx() );
558 QVariantList srcVars = QVariantList() << eww->value();
559 QList<int> fieldIndexes = QList<int>() << eww->fieldIdx();
560
561 // append additional fields
562 const QStringList additionalFields = eww->additionalFields();
563 for ( const QString &fieldName : additionalFields )
564 {
565 int idx = eww->layer()->fields().lookupField( fieldName );
566 fieldIndexes << idx;
567 dstVars << featureAttributes.at( idx );
568 }
569 srcVars.append( eww->additionalFieldValues() );
570
571 Q_ASSERT( dstVars.count() == srcVars.count() );
572
573 for ( int i = 0; i < dstVars.count(); i++ )
574 {
575 if ( !qgsVariantEqual( dstVars[i], srcVars[i] ) && srcVars[i].isValid() )
576 featureAttributes[fieldIndexes[i]] = srcVars[i];
577 }
578 }
579 updatedFeature.setAttributes( featureAttributes );
580
581 return updatedFeature;
582}
583
584void QgsAttributeForm::updateValuesDependencies( const int originIdx )
585{
586 updateValuesDependenciesDefaultValues( originIdx );
587 updateValuesDependenciesVirtualFields( originIdx );
588}
589
590void QgsAttributeForm::updateValuesDependenciesDefaultValues( const int originIdx )
591{
592 if ( !mDefaultValueDependencies.contains( originIdx ) )
593 return;
594
595 if ( !mFeature.isValid() )
596 {
597 switch ( mMode )
598 {
605 return;
606
609 break;
610 }
611 }
612
613 // create updated Feature
614 QgsFeature updatedFeature = getUpdatedFeature();
615
616 // go through depending fields and update the fields with defaultexpression
617 QList<QgsWidgetWrapper *> relevantWidgets = mDefaultValueDependencies.values( originIdx );
618 for ( QgsWidgetWrapper *ww : std::as_const( relevantWidgets ) )
619 {
620 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
621 if ( eww )
622 {
623 // Update only on form opening (except when applyOnUpdate is activated)
624 if ( mValuesInitialized && !eww->field().defaultValueDefinition().applyOnUpdate() )
625 continue;
626
627 // Update only when mMode is AddFeatureMode (except when applyOnUpdate is activated)
629 {
630 continue;
631 }
632
633 //do not update when this widget is already updating (avoid recursions)
634 if ( mAlreadyUpdatedFields.contains( eww->fieldIdx() ) )
635 continue;
636
637 QgsExpressionContext context = createExpressionContext( updatedFeature );
638
639 const QVariant value = mLayer->defaultValue( eww->fieldIdx(), updatedFeature, &context );
640 eww->setValue( value );
641 mCurrentFormFeature.setAttribute( eww->field().name(), value );
642 }
643 }
644}
645
646void QgsAttributeForm::updateValuesDependenciesVirtualFields( const int originIdx )
647{
648 if ( !mVirtualFieldsDependencies.contains( originIdx ) )
649 return;
650
651 if ( !mFeature.isValid() )
652 return;
653
654 // create updated Feature
655 QgsFeature updatedFeature = getUpdatedFeature();
656
657 // go through depending fields and update the virtual field with its expression
658 const QList<QgsWidgetWrapper *> relevantWidgets = mVirtualFieldsDependencies.values( originIdx );
659 for ( QgsWidgetWrapper *ww : relevantWidgets )
660 {
661 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
662 if ( !eww )
663 continue;
664
665 //do not update when this widget is already updating (avoid recursions)
666 if ( mAlreadyUpdatedFields.contains( eww->fieldIdx() ) )
667 continue;
668
669 // Update value
670 QgsExpressionContext context = createExpressionContext( updatedFeature );
671 QgsExpression exp( mLayer->expressionField( eww->fieldIdx() ) );
672 const QVariant value = exp.evaluate( &context );
673 updatedFeature.setAttribute( eww->fieldIdx(), value );
674 eww->setValue( value );
675 }
676}
677
678void QgsAttributeForm::updateValuesDependenciesParent()
679{
680 // create updated Feature
681 QgsFeature updatedFeature = getUpdatedFeature();
682 QList<int> updatedFields;
683
684 // go through fields dependent to parent feature value(s)
685 const QSet<QgsEditorWidgetWrapper *> relevantWidgets = mParentDependencies;
686 for ( QgsEditorWidgetWrapper *eww : relevantWidgets )
687 {
688 //do not update when this widget is already updating (avoid recursions)
689 if ( updatedFields.contains( eww->fieldIdx() ) )
690 continue;
691
692 // Update value
693 updatedFields << eww->fieldIdx();
694 QgsExpressionContext context = createExpressionContext( updatedFeature );
695 const QVariant value = mLayer->defaultValue( eww->fieldIdx(), updatedFeature, &context );
696 eww->setValue( value );
697 }
698}
699
700void QgsAttributeForm::updateRelatedLayerFields()
701{
702 // Synchronize dependencies
703 updateRelatedLayerFieldsDependencies();
704
705 if ( mRelatedLayerFieldsDependencies.isEmpty() )
706 return;
707
708 if ( !mFeature.isValid() )
709 return;
710
711 // create updated Feature
712 QgsFeature updatedFeature = getUpdatedFeature();
713
714 // go through depending fields and update the fields with virtual field
715 const QSet<QgsEditorWidgetWrapper *> relevantWidgets = mRelatedLayerFieldsDependencies;
716 for ( QgsEditorWidgetWrapper *eww : relevantWidgets )
717 {
718 //do not update when this widget is already updating (avoid recursions)
719 if ( mAlreadyUpdatedFields.contains( eww->fieldIdx() ) )
720 continue;
721
722 // Update value
723 QgsExpressionContext context = createExpressionContext( updatedFeature );
724 QgsExpression exp( mLayer->expressionField( eww->fieldIdx() ) );
725 QVariant value = exp.evaluate( &context );
726 eww->setValue( value );
727 }
728}
729
730void QgsAttributeForm::resetMultiEdit( bool promptToSave )
731{
732 if ( promptToSave )
733 save();
734
735 mUnsavedMultiEditChanges = false;
736 setMultiEditFeatureIds( mLayer->selectedFeatureIds() );
737}
738
739void QgsAttributeForm::multiEditMessageClicked( const QString &link )
740{
741 clearMultiEditMessages();
742 resetMultiEdit( link == "#apply"_L1 );
743}
744
745void QgsAttributeForm::filterTriggered()
746{
747 QString filter = createFilterExpression();
748 emit filterExpressionSet( filter, ReplaceFilter );
749 if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
751}
752
753void QgsAttributeForm::searchZoomTo()
754{
755 QString filter = createFilterExpression();
756 if ( filter.isEmpty() )
757 return;
758
759 emit zoomToFeatures( filter );
760}
761
762void QgsAttributeForm::searchFlash()
763{
764 QString filter = createFilterExpression();
765 if ( filter.isEmpty() )
766 return;
767
768 emit flashFeatures( filter );
769}
770
771void QgsAttributeForm::filterAndTriggered()
772{
773 QString filter = createFilterExpression();
774 if ( filter.isEmpty() )
775 return;
776
777 if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
779 emit filterExpressionSet( filter, FilterAnd );
780}
781
782void QgsAttributeForm::filterOrTriggered()
783{
784 QString filter = createFilterExpression();
785 if ( filter.isEmpty() )
786 return;
787
788 if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
790 emit filterExpressionSet( filter, FilterOr );
791}
792
793void QgsAttributeForm::pushSelectedFeaturesMessage()
794{
795 int count = mLayer->selectedFeatureCount();
796 if ( count > 0 )
797 {
798 mMessageBar->pushMessage( QString(), tr( "%n matching feature(s) selected", "matching features", count ), Qgis::MessageLevel::Info );
799 }
800 else
801 {
802 mMessageBar->pushMessage( QString(), tr( "No matching features found" ), Qgis::MessageLevel::Info );
803 }
804}
805
806void QgsAttributeForm::displayWarning( const QString &message )
807{
808 mMessageBar->pushMessage( QString(), message, Qgis::MessageLevel::Warning );
809}
810
811void QgsAttributeForm::runSearchSelect( Qgis::SelectBehavior behavior )
812{
813 QString filter = createFilterExpression();
814 if ( filter.isEmpty() )
815 return;
816
817 mLayer->selectByExpression( filter, behavior );
818 pushSelectedFeaturesMessage();
819 if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
821}
822
823void QgsAttributeForm::searchSetSelection()
824{
825 runSearchSelect( Qgis::SelectBehavior::SetSelection );
826}
827
828void QgsAttributeForm::searchAddToSelection()
829{
830 runSearchSelect( Qgis::SelectBehavior::AddToSelection );
831}
832
833void QgsAttributeForm::searchRemoveFromSelection()
834{
836}
837
838void QgsAttributeForm::searchIntersectSelection()
839{
841}
842
843bool QgsAttributeForm::saveMultiEdits()
844{
845 //find changed attributes
846 QgsAttributeMap newAttributeValues;
847 const QList<int> fieldIndexes = mFormEditorWidgets.uniqueKeys();
848 for ( int fieldIndex : fieldIndexes )
849 {
850 const QList<QgsAttributeFormEditorWidget *> widgets = mFormEditorWidgets.values( fieldIndex );
851 if ( !widgets.first()->hasChanged() )
852 continue;
853
854 if ( !widgets.first()->currentValue().isValid() // if the widget returns invalid (== do not change)
855 || !fieldIsEditable( fieldIndex ) ) // or the field cannot be edited ...
856 {
857 continue;
858 }
859
860 // let editor know we've accepted the changes
861 for ( QgsAttributeFormEditorWidget *widget : widgets )
862 widget->changesCommitted();
863
864 newAttributeValues.insert( fieldIndex, widgets.first()->currentValue() );
865 }
866
867 if ( newAttributeValues.isEmpty() )
868 {
869 //nothing to change
870 return true;
871 }
872
873#if 0
874 // prompt for save
875 int res = QMessageBox::information( this, tr( "Multiedit Attributes" ),
876 tr( "Edits will be applied to all selected features." ), QMessageBox::Ok | QMessageBox::Cancel );
877 if ( res != QMessageBox::Ok )
878 {
879 resetMultiEdit();
880 return false;
881 }
882#endif
883
884 mLayer->beginEditCommand( tr( "Updated multiple feature attributes" ) );
885
886 bool success = true;
887
888 const auto constMultiEditFeatureIds = mMultiEditFeatureIds;
889 for ( QgsFeatureId fid : constMultiEditFeatureIds )
890 {
891 QgsAttributeMap::const_iterator aIt = newAttributeValues.constBegin();
892 for ( ; aIt != newAttributeValues.constEnd(); ++aIt )
893 {
894 success &= mLayer->changeAttributeValue( fid, aIt.key(), aIt.value() );
895 }
896 }
897
898 clearMultiEditMessages();
899 if ( success )
900 {
901 mLayer->endEditCommand();
902 mLayer->triggerRepaint();
903 mMultiEditMessageBarItem = new QgsMessageBarItem( tr( "Attribute changes for multiple features applied." ), Qgis::MessageLevel::Success, -1 );
904 }
905 else
906 {
907 mLayer->destroyEditCommand();
908 mMultiEditMessageBarItem = new QgsMessageBarItem( tr( "Changes could not be applied." ), Qgis::MessageLevel::Warning, 0 );
909 }
910
911 if ( !mButtonBox->isVisible() )
912 mMessageBar->pushItem( mMultiEditMessageBarItem );
913 return success;
914}
915
917{
918 return saveWithDetails( nullptr );
919}
920
922{
923 if ( error )
924 error->clear();
925
926 if ( mIsSaving )
927 return true;
928
929 if ( mContext.formMode() == QgsAttributeEditorContext::Embed && !mValidConstraints )
930 {
931 // the feature isn't saved (as per the warning provided), but we return true
932 // so switching features still works
933 return true;
934 }
935
936 for ( QgsWidgetWrapper *wrapper : std::as_const( mWidgets ) )
937 {
938 wrapper->notifyAboutToSave();
939 }
940
941 // only do the dirty checks when editing an existing feature - for new
942 // features we need to add them even if the attributes are unchanged from the initial
943 // default values
944 switch ( mMode )
945 {
950 if ( !mDirty )
951 return true;
952 break;
953
957 break;
958
960 return true;
961 }
962
963 mIsSaving = true;
964
965 bool success = true;
966
967 emit beforeSave( success );
968
969 // Somebody wants to prevent this form from saving
970 if ( !success )
971 return false;
972
973 switch ( mMode )
974 {
981 success = saveEdits( error );
982 break;
983
985 success = saveMultiEdits();
986 break;
987
989 break;
990 }
991
992 mIsSaving = false;
993 mUnsavedMultiEditChanges = false;
994 mDirty = false;
995
996 return success;
997}
998
999
1001{
1002 mValuesInitialized = false;
1003 const auto constMWidgets = mWidgets;
1004 for ( QgsWidgetWrapper *ww : constMWidgets )
1005 {
1006 ww->setFeature( mFeature );
1007 }
1008
1009 // Update dependent virtual fields (not default values / not referencing layer values)
1010 for ( QgsWidgetWrapper *ww : constMWidgets )
1011 {
1012 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1013 if ( !eww )
1014 continue;
1015
1016 // Append field index here, so it's not updated recursively
1017 mAlreadyUpdatedFields.append( eww->fieldIdx() );
1018 updateValuesDependenciesVirtualFields( eww->fieldIdx() );
1019 mAlreadyUpdatedFields.removeAll( eww->fieldIdx() );
1020 }
1021
1022 mValuesInitialized = true;
1023 mDirty = false;
1024}
1025
1027{
1028 const auto widgets { findChildren<QgsAttributeFormEditorWidget *>() };
1029 for ( QgsAttributeFormEditorWidget *w : widgets )
1030 {
1031 w->resetSearch();
1032 }
1033}
1034
1035void QgsAttributeForm::clearMultiEditMessages()
1036{
1037 if ( mMultiEditUnsavedMessageBarItem )
1038 {
1039 if ( !mButtonBox->isVisible() )
1040 mMessageBar->popWidget( mMultiEditUnsavedMessageBarItem );
1041 mMultiEditUnsavedMessageBarItem = nullptr;
1042 }
1043 if ( mMultiEditMessageBarItem )
1044 {
1045 if ( !mButtonBox->isVisible() )
1046 mMessageBar->popWidget( mMultiEditMessageBarItem );
1047 mMultiEditMessageBarItem = nullptr;
1048 }
1049}
1050
1051QString QgsAttributeForm::createFilterExpression() const
1052{
1053 QStringList filters;
1054 for ( QgsAttributeFormWidget *w : std::as_const( mFormWidgets ) )
1055 {
1056 QString filter = w->currentFilterExpression();
1057 if ( !filter.isEmpty() )
1058 filters << filter;
1059 }
1060
1061 if ( filters.isEmpty() )
1062 return QString();
1063
1064 QString filter = filters.join( ") AND ("_L1 ).prepend( '(' ).append( ')' );
1065 return filter;
1066}
1067
1068QgsExpressionContext QgsAttributeForm::createExpressionContext( const QgsFeature &feature ) const
1069{
1070 QgsExpressionContext context;
1072 context.appendScope( QgsExpressionContextUtils::formScope( feature, mContext.attributeFormModeString() ) );
1073 if ( mExtraContextScope )
1074 {
1075 context.appendScope( new QgsExpressionContextScope( *mExtraContextScope.get() ) );
1076 }
1077 if ( mContext.parentFormFeature().isValid() )
1078 {
1079 context.appendScope( QgsExpressionContextUtils::parentFormScope( mContext.parentFormFeature() ) );
1080 }
1081 context.setFeature( feature );
1082 return context;
1083}
1084
1085
1086void QgsAttributeForm::onAttributeChanged( const QVariant &value, const QVariantList &additionalFieldValues )
1087{
1088 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( sender() );
1089 Q_ASSERT( eww );
1090
1091 bool signalEmitted = false;
1092
1093 if ( mValuesInitialized )
1094 mDirty = true;
1095
1096 mCurrentFormFeature.setAttribute( eww->field().name(), value );
1097
1098 // Update other widgets pointing to the same field, required to happen now to insure
1099 // currentFormValuesFeature() gets the right value when processing constraints
1100 const QList<QgsAttributeFormEditorWidget *> formEditorWidgets = mFormEditorWidgets.values( eww->fieldIdx() );
1101 for ( QgsAttributeFormEditorWidget *formEditorWidget : std::as_const( formEditorWidgets ) )
1102 {
1103 if ( formEditorWidget->editorWidget() == eww )
1104 continue;
1105
1106 // formEditorWidget and eww points to the same field, so update its value
1107 formEditorWidget->editorWidget()->setValue( value );
1108 }
1109
1110 switch ( mMode )
1111 {
1117 {
1119 emit attributeChanged( eww->field().name(), value );
1121 emit widgetValueChanged( eww->field().name(), value, !mIsSettingFeature );
1122
1123 // also emit the signal for additional values
1124 const QStringList additionalFields = eww->additionalFields();
1125 for ( int i = 0; i < additionalFields.count(); i++ )
1126 {
1127 const QString fieldName = additionalFields.at( i );
1128 const QVariant value = additionalFieldValues.at( i );
1129 emit widgetValueChanged( fieldName, value, !mIsSettingFeature );
1130 }
1131
1132 signalEmitted = true;
1133
1134 if ( mValuesInitialized )
1135 updateJoinedFields( *eww );
1136
1137 break;
1138 }
1140 {
1141 if ( !mIsSettingMultiEditFeatures )
1142 {
1143 mUnsavedMultiEditChanges = true;
1144
1145 QLabel *msgLabel = new QLabel( tr( "Unsaved multiedit changes: <a href=\"#apply\">apply changes</a> or <a href=\"#reset\">reset changes</a>." ), mMessageBar );
1146 msgLabel->setAlignment( Qt::AlignLeft | Qt::AlignVCenter );
1147 msgLabel->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );
1148 connect( msgLabel, &QLabel::linkActivated, this, &QgsAttributeForm::multiEditMessageClicked );
1149 clearMultiEditMessages();
1150
1151 mMultiEditUnsavedMessageBarItem = new QgsMessageBarItem( msgLabel, Qgis::MessageLevel::Warning );
1152 if ( !mButtonBox->isVisible() )
1153 mMessageBar->pushItem( mMultiEditUnsavedMessageBarItem );
1154
1155 emit widgetValueChanged( eww->field().name(), value, false );
1156 signalEmitted = true;
1157 }
1158 break;
1159 }
1162 //nothing to do
1163 break;
1164 }
1165
1166 updateConstraints( eww );
1167
1168 // Update dependent fields (only if form is not initializing)
1169 if ( mValuesInitialized && !mIsSettingMultiEditFeatures )
1170 {
1171 //append field index here, so it's not updated recursive
1172 mAlreadyUpdatedFields.append( eww->fieldIdx() );
1173 updateValuesDependencies( eww->fieldIdx() );
1174 mAlreadyUpdatedFields.removeAll( eww->fieldIdx() );
1175 }
1176
1177 // Updates expression controlled labels and editable state
1178 updateLabels();
1179 updateEditableState();
1180
1181 if ( !signalEmitted )
1182 {
1184 emit attributeChanged( eww->field().name(), value );
1186 bool attributeHasChanged = !mIsSettingFeature;
1188 attributeHasChanged &= !mIsSettingMultiEditFeatures;
1189
1190 emit widgetValueChanged( eww->field().name(), value, attributeHasChanged );
1191 }
1192}
1193
1194void QgsAttributeForm::updateAllConstraints()
1195{
1196 const auto constMWidgets = mWidgets;
1197 for ( QgsWidgetWrapper *ww : constMWidgets )
1198 {
1199 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1200 if ( eww )
1201 updateConstraints( eww );
1202 }
1203}
1204
1205void QgsAttributeForm::updateConstraints( QgsEditorWidgetWrapper *eww )
1206{
1207 // get the current feature set in the form
1208 QgsFeature ft;
1209 if ( currentFormValuesFeature( ft ) )
1210 {
1211 // if the layer is NOT being edited then we only check layer based constraints, and not
1212 // any constraints enforced by the provider. Because:
1213 // 1. we want to keep browsing features nice and responsive. It's nice to give feedback as to whether
1214 // the value checks out, but not if it's too slow to do so. Some constraints (e.g., unique) can be
1215 // expensive to test. A user can freely remove a layer-based constraint if it proves to be too slow
1216 // to test, but they are unlikely to have any control over provider-side constraints
1217 // 2. the provider has already accepted the value, so presumably it doesn't violate the constraint
1218 // and there's no point rechecking!
1219
1220 // update eww constraint
1221 updateConstraint( ft, eww );
1222
1223 // update eww dependencies constraint
1224 const QList<QgsEditorWidgetWrapper *> deps = constraintDependencies( eww );
1225
1226 for ( QgsEditorWidgetWrapper *depsEww : deps )
1227 updateConstraint( ft, depsEww );
1228
1229 // sync OK button status
1230 synchronizeState();
1231
1232 QgsExpressionContext context = createExpressionContext( ft );
1233
1234 // Recheck visibility/collapsed state for all containers which are controlled by this value
1235 const QVector<ContainerInformation *> infos = mContainerInformationDependency.value( eww->field().name() );
1236 for ( ContainerInformation *info : infos )
1237 {
1238 info->apply( &context );
1239 }
1240 }
1241}
1242
1243void QgsAttributeForm::updateContainersVisibility()
1244{
1245 QgsExpressionContext context = createExpressionContext( mFeature );
1246
1247 const QVector<ContainerInformation *> infos = mContainerVisibilityCollapsedInformation;
1248
1249 for ( ContainerInformation *info : infos )
1250 {
1251 info->apply( &context );
1252 }
1253
1254 // Update the constraints if not in multi edit, because
1255 // when mode changes to multi edit, constraints have been already
1256 // updated and a further update will use current form feature values,
1257 // possibly empty for mixed values, leading to false positive
1258 // constraints violations.
1260 {
1261 updateAllConstraints();
1262 }
1263}
1264
1265void QgsAttributeForm::updateConstraint( const QgsFeature &ft, QgsEditorWidgetWrapper *eww )
1266{
1268
1269 if ( eww->layer()->fields().fieldOrigin( eww->fieldIdx() ) == Qgis::FieldOrigin::Join )
1270 {
1271 int srcFieldIdx;
1272 const QgsVectorLayerJoinInfo *info = eww->layer()->joinBuffer()->joinForFieldIndex( eww->fieldIdx(), eww->layer()->fields(), srcFieldIdx );
1273
1274 if ( info && info->joinLayer() && info->isDynamicFormEnabled() )
1275 {
1276 if ( mJoinedFeatures.contains( info ) )
1277 {
1278 eww->updateConstraint( info->joinLayer(), srcFieldIdx, mJoinedFeatures[info], constraintOrigin );
1279 return;
1280 }
1281 else // if we are here, it means there's not joined field for this feature
1282 {
1283 eww->updateConstraint( QgsFeature() );
1284 return;
1285 }
1286 }
1287 }
1288 // default constraint update
1289 eww->updateConstraint( ft, constraintOrigin );
1290}
1291
1292void QgsAttributeForm::updateLabels()
1293{
1294 if ( !mLabelDataDefinedProperties.isEmpty() )
1295 {
1296 QgsFeature currentFeature;
1297 if ( currentFormValuesFeature( currentFeature ) )
1298 {
1299 QgsExpressionContext context = createExpressionContext( currentFeature );
1300
1301 for ( auto it = mLabelDataDefinedProperties.constBegin(); it != mLabelDataDefinedProperties.constEnd(); ++it )
1302 {
1303 QLabel *label { it.key() };
1304 bool ok;
1305 const QString value { it->valueAsString( context, QString(), &ok ) };
1306 if ( ok && !value.isEmpty() )
1307 {
1308 label->setText( value );
1309 }
1310 }
1311 }
1312 }
1313 if ( !mCustomCommentDataDefinedProperties.isEmpty() )
1314 {
1315 QgsFeature currentFeature;
1316 if ( currentFormValuesFeature( currentFeature ) )
1317 {
1318 QgsExpressionContext context = createExpressionContext( currentFeature );
1319
1320 for ( auto it = mCustomCommentDataDefinedProperties.constBegin(); it != mCustomCommentDataDefinedProperties.constEnd(); ++it )
1321 {
1322 QWidget *labelWidget { it.key() };
1323 bool ok;
1324 const QString value { it->valueAsString( context, QString(), &ok ) };
1325 if ( ok && !value.isEmpty() )
1326 {
1327 const QString fieldName = labelWidget->objectName();
1328 const QgsFields fields = mLayer->fields();
1329 int idx = fields.lookupField( fieldName );
1330
1331 if ( idx >= 0 )
1332 {
1333 labelWidget->setToolTip( QgsFieldModel::fieldToolTipExtended( fields.at( idx ), mLayer, value ) );
1334 }
1335 }
1336 }
1337 }
1338 }
1339}
1340
1341void QgsAttributeForm::updateEditableState()
1342{
1343 if ( !mEditableDataDefinedProperties.isEmpty() )
1344 {
1345 QgsFeature currentFeature;
1346 if ( currentFormValuesFeature( currentFeature ) )
1347 {
1348 QgsExpressionContext context = createExpressionContext( currentFeature );
1349
1350 for ( auto it = mEditableDataDefinedProperties.constBegin(); it != mEditableDataDefinedProperties.constEnd(); ++it )
1351 {
1352 QWidget *w { it.key() };
1353 bool ok;
1354 const bool isEditable { it->valueAsBool( context, true, &ok ) && mLayer && mLayer->isEditable() }; // *NOPAD*
1355 if ( ok )
1356 {
1357 QgsAttributeFormEditorWidget *editorWidget { qobject_cast<QgsAttributeFormEditorWidget *>( w ) };
1358 if ( editorWidget )
1359 {
1360 editorWidget->editorWidget()->setEnabled( isEditable );
1361 }
1362 else
1363 {
1364 w->setEnabled( isEditable );
1365 }
1366 }
1367 }
1368 }
1369 }
1370}
1371
1372bool QgsAttributeForm::currentFormValuesFeature( QgsFeature &feature )
1373{
1374 bool rc = true;
1375 feature = QgsFeature( mFeature );
1376 QgsAttributes dst = feature.attributes();
1377
1378 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
1379 {
1380 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1381
1382 if ( !eww )
1383 continue;
1384
1385 if ( dst.count() > eww->fieldIdx() )
1386 {
1387 QVariantList dstVars = QVariantList() << dst.at( eww->fieldIdx() );
1388 QVariantList srcVars = QVariantList() << eww->value();
1389 QList<int> fieldIndexes = QList<int>() << eww->fieldIdx();
1390
1391 // append additional fields
1392 const QStringList additionalFields = eww->additionalFields();
1393 for ( const QString &fieldName : additionalFields )
1394 {
1395 int idx = eww->layer()->fields().lookupField( fieldName );
1396 fieldIndexes << idx;
1397 dstVars << dst.at( idx );
1398 }
1399 srcVars.append( eww->additionalFieldValues() );
1400
1401 Q_ASSERT( dstVars.count() == srcVars.count() );
1402
1403 for ( int i = 0; i < dstVars.count(); i++ )
1404 {
1405 // need to check dstVar.isNull() != srcVar.isNull()
1406 // otherwise if dstVar=NULL and scrVar=0, then dstVar = srcVar
1407 if ( ( !qgsVariantEqual( dstVars[i], srcVars[i] ) || QgsVariantUtils::isNull( dstVars[i] ) != QgsVariantUtils::isNull( srcVars[i] ) ) && srcVars[i].isValid() )
1408 {
1409 dst[fieldIndexes[i]] = srcVars[i];
1410 }
1411 }
1412 }
1413 else
1414 {
1415 rc = false;
1416 break;
1417 }
1418 }
1419
1420 feature.setAttributes( dst );
1421
1422 return rc;
1423}
1424
1425
1426void QgsAttributeForm::registerContainerInformation( QgsAttributeForm::ContainerInformation *info )
1427{
1428 mContainerVisibilityCollapsedInformation.append( info );
1429
1430 const QSet<QString> referencedColumns = info->expression.referencedColumns().unite( info->collapsedExpression.referencedColumns() );
1431
1432 for ( const QString &col : referencedColumns )
1433 {
1434 mContainerInformationDependency[col].append( info );
1435 }
1436}
1437
1438bool QgsAttributeForm::currentFormValidConstraints( QStringList &invalidFields, QStringList &descriptions ) const
1439{
1440 bool valid { true };
1441
1442 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
1443 {
1444 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1445 if ( eww )
1446 {
1447 if ( !eww->isValidConstraint() )
1448 {
1449 invalidFields.append( eww->field().displayName() );
1450
1451 descriptions.append( eww->constraintFailureReason() );
1452
1453 if ( eww->isBlockingCommit() )
1454 valid = false; // continue to get all invalid fields
1455 }
1456 }
1457 }
1458
1459 return valid;
1460}
1461
1462bool QgsAttributeForm::currentFormValidHardConstraints( QStringList &invalidFields, QStringList &descriptions ) const
1463{
1464 bool valid { true };
1465
1466 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
1467 {
1468 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1469 if ( eww )
1470 {
1471 if ( eww->isBlockingCommit() )
1472 {
1473 invalidFields.append( eww->field().displayName() );
1474 descriptions.append( eww->constraintFailureReason() );
1475 valid = false; // continue to get all invalid fields
1476 }
1477 }
1478 }
1479
1480 return valid;
1481}
1482
1483void QgsAttributeForm::onAttributeAdded( int idx )
1484{
1485 mPreventFeatureRefresh = false;
1486 if ( mFeature.isValid() )
1487 {
1488 QgsAttributes attrs = mFeature.attributes();
1489 attrs.insert( idx, QgsVariantUtils::createNullVariant( layer()->fields().at( idx ).type() ) );
1490 mFeature.setFields( layer()->fields() );
1491 mFeature.setAttributes( attrs );
1492 }
1493 init();
1494 setFeature( mFeature );
1495}
1496
1497void QgsAttributeForm::onAttributeDeleted( int idx )
1498{
1499 mPreventFeatureRefresh = false;
1500 if ( mFeature.isValid() )
1501 {
1502 QgsAttributes attrs = mFeature.attributes();
1503 attrs.remove( idx );
1504 mFeature.setFields( layer()->fields() );
1505 mFeature.setAttributes( attrs );
1506 }
1507 init();
1508 setFeature( mFeature );
1509}
1510
1511void QgsAttributeForm::onRelatedFeaturesChanged()
1512{
1513 updateRelatedLayerFields();
1514}
1515
1516void QgsAttributeForm::onUpdatedFields()
1517{
1518 mPreventFeatureRefresh = false;
1519 if ( mFeature.isValid() )
1520 {
1521 QgsAttributes attrs( layer()->fields().size() );
1522 for ( int i = 0; i < layer()->fields().size(); i++ )
1523 {
1524 int idx = mFeature.fields().indexFromName( layer()->fields().at( i ).name() );
1525 if ( idx != -1 )
1526 {
1527 attrs[i] = mFeature.attributes().at( idx );
1528 if ( mFeature.attributes().at( idx ).userType() != layer()->fields().at( i ).type() )
1529 {
1530 attrs[i].convert( layer()->fields().at( i ).type() );
1531 }
1532 }
1533 else
1534 {
1535 attrs[i] = QgsVariantUtils::createNullVariant( layer()->fields().at( i ).type() );
1536 }
1537 }
1538 mFeature.setFields( layer()->fields() );
1539 mFeature.setAttributes( attrs );
1540 }
1541 init();
1542 setFeature( mFeature );
1543}
1544
1545void QgsAttributeForm::onConstraintStatusChanged( const QString &constraint, const QString &description, const QString &err, QgsEditorWidgetWrapper::ConstraintResult result )
1546{
1547 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( sender() );
1548 Q_ASSERT( eww );
1549
1550 const QList<QgsAttributeFormEditorWidget *> formEditorWidgets = mFormEditorWidgets.values( eww->fieldIdx() );
1551
1552 for ( QgsAttributeFormEditorWidget *formEditorWidget : formEditorWidgets )
1553 {
1554 formEditorWidget->setConstraintStatus( constraint, description, err, result );
1555 if ( formEditorWidget->editorWidget() != eww )
1556 {
1557 formEditorWidget->editorWidget()->updateConstraint( result, err );
1558 }
1559 }
1560}
1561
1562QList<QgsEditorWidgetWrapper *> QgsAttributeForm::constraintDependencies( QgsEditorWidgetWrapper *w )
1563{
1564 QList<QgsEditorWidgetWrapper *> wDeps;
1565 QString name = w->field().name();
1566
1567 // for each widget in the current form
1568 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
1569 {
1570 // get the wrapper
1571 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1572 if ( eww )
1573 {
1574 // compare name to not compare w to itself
1575 QString ewwName = eww->field().name();
1576 if ( name != ewwName )
1577 {
1578 // get expression and referencedColumns
1579 QgsExpression expr = eww->layer()->fields().at( eww->fieldIdx() ).constraints().constraintExpression();
1580
1581 const auto referencedColumns = expr.referencedColumns();
1582
1583 for ( const QString &colName : referencedColumns )
1584 {
1585 if ( name.compare( colName, Qt::CaseSensitivity::CaseInsensitive ) == 0 )
1586 {
1587 wDeps.append( eww );
1588 break;
1589 }
1590 }
1591 }
1592 }
1593 }
1594
1595 return wDeps;
1596}
1597
1598QgsRelationWidgetWrapper *QgsAttributeForm::setupRelationWidgetWrapper( const QgsRelation &rel, const QgsAttributeEditorContext &context )
1599{
1600 return setupRelationWidgetWrapper( QString(), rel, context );
1601}
1602
1603QgsRelationWidgetWrapper *QgsAttributeForm::setupRelationWidgetWrapper( const QString &relationWidgetTypeId, const QgsRelation &rel, const QgsAttributeEditorContext &context )
1604{
1605 QgsRelationWidgetWrapper *rww = new QgsRelationWidgetWrapper( relationWidgetTypeId, mLayer, rel, nullptr, this );
1606 const QVariantMap config = mLayer->editFormConfig().widgetConfig( rel.id() );
1607 rww->setConfig( config );
1608 rww->setContext( context );
1609
1610 return rww;
1611}
1612
1613QToolButton *QgsAttributeForm::createCommentInfoButton( QWidget *labelWidget )
1614{
1615 QToolButton *infoButton = new QToolButton( labelWidget );
1616 infoButton->setIcon( QgsApplication::getThemeIcon( u"/mIndicatorFieldComment.svg"_s, QgsApplication::palette().color( QPalette::WindowText ) ) );
1617 infoButton->setFocusPolicy( Qt::NoFocus );
1618 // looks like an icon, but on click the tooltip of the label appears
1619 infoButton->setAutoRaise( true );
1620 infoButton->setStyleSheet(
1621 "QToolButton { "
1622 " border: none; "
1623 " background-color: transparent; "
1624 " padding: 0px; "
1625 "}"
1626 );
1627 // connect button to label (for the label's tooltip)
1628 connect( infoButton, &QToolButton::clicked, labelWidget, [labelWidget]() { QToolTip::showText( QCursor::pos(), labelWidget->toolTip(), labelWidget ); } );
1629 // invisible per default
1630 infoButton->setVisible( false );
1631 return infoButton;
1632}
1633
1634void QgsAttributeForm::preventFeatureRefresh()
1635{
1636 mPreventFeatureRefresh = true;
1637}
1638
1640{
1641 if ( mPreventFeatureRefresh || mLayer->isEditable() || !mFeature.isValid() )
1642 return;
1643
1644 // reload feature if layer changed although not editable
1645 // (datasource probably changed bypassing QgsVectorLayer)
1646 if ( !mLayer->getFeatures( QgsFeatureRequest().setFilterFid( mFeature.id() ) ).nextFeature( mFeature ) )
1647 return;
1648
1649 init();
1650 setFeature( mFeature );
1651}
1652
1653void QgsAttributeForm::parentFormValueChanged( const QString &attribute, const QVariant &newValue )
1654{
1655 if ( mContext.parentFormFeature().isValid() )
1656 {
1657 QgsFeature parentFormFeature = mContext.parentFormFeature();
1658 parentFormFeature.setAttribute( attribute, newValue );
1659 mContext.setParentFormFeature( parentFormFeature );
1660 }
1661
1662 updateValuesDependenciesParent();
1663
1664 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
1665 {
1666 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1667 if ( eww )
1668 {
1669 eww->parentFormValueChanged( attribute, newValue );
1670 }
1671 }
1672}
1673
1675{
1676 return mNeedsGeometry;
1677}
1678
1679void QgsAttributeForm::synchronizeState()
1680{
1681 bool isEditable = false;
1682 switch ( mMode )
1683 {
1690 isEditable = mFeature.isValid() && mLayer->isEditable();
1691 break;
1692
1694 isEditable = mLayer->isEditable();
1695 break;
1696
1698 isEditable = true;
1699 break;
1700 }
1701
1702 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
1703 {
1704 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1705 if ( eww )
1706 {
1707 const QList<QgsAttributeFormEditorWidget *> formWidgets = mFormEditorWidgets.values( eww->fieldIdx() );
1708
1709 for ( QgsAttributeFormEditorWidget *formWidget : formWidgets )
1710 {
1711 formWidget->setConstraintResultVisible( isEditable );
1712 }
1713
1714 eww->setConstraintResultVisible( isEditable );
1715
1716 bool enabled = isEditable && fieldIsEditable( eww->fieldIdx() );
1717 ww->setEnabled( enabled );
1718
1719 updateIcon( eww );
1720 }
1721 else // handle QgsWidgetWrapper different than QgsEditorWidgetWrapper
1722 {
1723 ww->setEnabled( isEditable );
1724 }
1725 }
1726
1727
1729 {
1730 if ( mMode == QgsAttributeEditorContext::Mode::MultiEditMode && mLayer->selectedFeatureCount() == 0 )
1731 {
1732 isEditable = false;
1733 if ( mConstraintsFailMessageBarItem )
1734 {
1735 mMessageBar->popWidget( mConstraintsFailMessageBarItem );
1736 }
1737 mConstraintsFailMessageBarItem = new QgsMessageBarItem( tr( "Multi edit mode requires at least one selected feature." ), Qgis::MessageLevel::Info, -1 );
1738 mMessageBar->pushItem( mConstraintsFailMessageBarItem );
1739 }
1740 else
1741 {
1742 QStringList invalidFields, descriptions;
1743 mValidConstraints = currentFormValidHardConstraints( invalidFields, descriptions );
1744
1745 if ( isEditable && mContext.formMode() == QgsAttributeEditorContext::Embed )
1746 {
1747 if ( !mValidConstraints && !mConstraintsFailMessageBarItem )
1748 {
1749 mConstraintsFailMessageBarItem
1750 = new QgsMessageBarItem( tr( "Changes to this form will not be saved. %n field(s) don't meet their constraints.", "invalid fields", invalidFields.size() ), Qgis::MessageLevel::Warning, -1 );
1751 mMessageBar->pushItem( mConstraintsFailMessageBarItem );
1752 }
1753 else if ( mValidConstraints && mConstraintsFailMessageBarItem )
1754 {
1755 mMessageBar->popWidget( mConstraintsFailMessageBarItem );
1756 mConstraintsFailMessageBarItem = nullptr;
1757 }
1758 }
1759 else if ( mConstraintsFailMessageBarItem )
1760 {
1761 mMessageBar->popWidget( mConstraintsFailMessageBarItem );
1762 mConstraintsFailMessageBarItem = nullptr;
1763 }
1764
1765 isEditable = isEditable & mValidConstraints;
1766 }
1767 }
1768
1769 // change OK button status
1770 QPushButton *okButton = mButtonBox->button( QDialogButtonBox::Ok );
1771 if ( okButton )
1772 {
1773 okButton->setEnabled( isEditable );
1774 }
1775}
1776
1777void QgsAttributeForm::init()
1778{
1779 QApplication::setOverrideCursor( QCursor( Qt::WaitCursor ) );
1780
1781 // Cleanup of any previously shown widget, we start from scratch
1782 QWidget *formWidget = nullptr;
1783 mNeedsGeometry = false;
1784
1785 bool buttonBoxVisible = true;
1786 // Cleanup button box but preserve visibility
1787 if ( mButtonBox )
1788 {
1789 buttonBoxVisible = mButtonBox->isVisible();
1790 delete mButtonBox;
1791 mButtonBox = nullptr;
1792 }
1793
1794 if ( mSearchButtonBox )
1795 {
1796 delete mSearchButtonBox;
1797 mSearchButtonBox = nullptr;
1798 }
1799
1800 qDeleteAll( mWidgets );
1801 mWidgets.clear();
1802
1803 while ( QWidget *w = this->findChild<QWidget *>() )
1804 {
1805 delete w;
1806 }
1807 delete layout();
1808
1809 QVBoxLayout *vl = new QVBoxLayout();
1810 vl->setContentsMargins( 0, 0, 0, 0 );
1811 mMessageBar = new QgsMessageBar( this );
1812 mMessageBar->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );
1813 vl->addWidget( mMessageBar );
1814
1815 setLayout( vl );
1816
1817 // Get a layout
1818 QGridLayout *layout = new QGridLayout();
1819 QWidget *container = new QWidget();
1820 container->setLayout( layout );
1821 vl->addWidget( container );
1822
1823 mFormEditorWidgets.clear();
1824 mFormWidgets.clear();
1825
1826 // a bar to warn the user with non-blocking messages
1827 setContentsMargins( 0, 0, 0, 0 );
1828
1829 // Try to load Ui-File for layout
1830 if ( mContext.allowCustomUi() && mLayer->editFormConfig().layout() == Qgis::AttributeFormLayout::UiFile && !mLayer->editFormConfig().uiForm().isEmpty() )
1831 {
1832 QgsDebugMsgLevel( u"loading form: %1"_s.arg( mLayer->editFormConfig().uiForm() ), 2 );
1833 const QString path = mLayer->editFormConfig().uiForm();
1835 if ( file && file->open( QFile::ReadOnly ) )
1836 {
1837 QUiLoader loader;
1838
1839 QFileInfo fi( file->fileName() );
1840 loader.setWorkingDirectory( fi.dir() );
1841 formWidget = loader.load( file, this );
1842 if ( formWidget )
1843 {
1844 formWidget->setWindowFlags( Qt::Widget );
1845 layout->addWidget( formWidget );
1846 formWidget->show();
1847 file->close();
1848 mButtonBox = findChild<QDialogButtonBox *>();
1849 createWrappers();
1850
1851 formWidget->installEventFilter( this );
1852 }
1853 }
1854 }
1855
1856 QgsTabWidget *tabWidget = nullptr;
1857
1858 // Tab layout
1859 if ( !formWidget && mLayer->editFormConfig().layout() == Qgis::AttributeFormLayout::DragAndDrop )
1860 {
1861 int row = 0;
1862 int column = 0;
1863 int columnCount = 1;
1864 bool hasRootFields = false;
1865 bool addSpacer = true;
1866
1867 const QList<QgsAttributeEditorElement *> tabs = mLayer->editFormConfig().tabs();
1868
1869 for ( QgsAttributeEditorElement *widgDef : tabs )
1870 {
1871 if ( widgDef->type() == Qgis::AttributeEditorType::Container )
1872 {
1873 QgsAttributeEditorContainer *containerDef = dynamic_cast<QgsAttributeEditorContainer *>( widgDef );
1874 if ( !containerDef )
1875 continue;
1876
1877 switch ( containerDef->type() )
1878 {
1880 {
1881 tabWidget = nullptr;
1882 WidgetInfo widgetInfo = createWidgetFromDef( widgDef, formWidget, mLayer, mContext );
1883 if ( widgetInfo.labelStyle.overrideColor )
1884 {
1885 if ( widgetInfo.labelStyle.color.isValid() )
1886 {
1887 widgetInfo.widget->setStyleSheet( u"QGroupBox::title { color: %1; }"_s.arg( widgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
1888 }
1889 }
1890 if ( widgetInfo.labelStyle.overrideFont )
1891 {
1892 widgetInfo.widget->setFont( widgetInfo.labelStyle.font );
1893 }
1894
1895 layout->addWidget( widgetInfo.widget, row, column, 1, 2 );
1896 if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
1897 {
1898 layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
1899 }
1900 if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
1901 {
1902 layout->setRowStretch( row, widgDef->verticalStretch() );
1903 addSpacer = false;
1904 }
1905 else
1906 {
1907 if ( widgetInfo.expandingNeeded )
1908 {
1909 addSpacer = false;
1910 widgetInfo.widget->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Expanding );
1911 }
1912 else
1913 {
1914 widgetInfo.widget->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Maximum );
1915 }
1916 }
1917
1918 if ( containerDef->visibilityExpression().enabled() || containerDef->collapsedExpression().enabled() )
1919 {
1920 registerContainerInformation( new ContainerInformation(
1921 widgetInfo.widget,
1922 containerDef->visibilityExpression().enabled() ? containerDef->visibilityExpression().data() : QgsExpression(),
1923 containerDef->collapsed(),
1924 containerDef->collapsedExpression().enabled() ? containerDef->collapsedExpression().data() : QgsExpression()
1925 ) );
1926 }
1927 column += 2;
1928 break;
1929 }
1930
1932 {
1933 tabWidget = nullptr;
1934 WidgetInfo widgetInfo = createWidgetFromDef( widgDef, formWidget, mLayer, mContext );
1935 layout->addWidget( widgetInfo.widget, row, column, 1, 2 );
1936 if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
1937 {
1938 layout->setRowStretch( row, widgDef->verticalStretch() );
1939 addSpacer = false;
1940 }
1941 else
1942 {
1943 if ( widgetInfo.expandingNeeded )
1944 {
1945 addSpacer = false;
1946 widgetInfo.widget->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Expanding );
1947 }
1948 else
1949 {
1950 widgetInfo.widget->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Maximum );
1951 }
1952 }
1953 if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
1954 {
1955 layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
1956 }
1957
1958 if ( containerDef->visibilityExpression().enabled() || containerDef->collapsedExpression().enabled() )
1959 {
1960 registerContainerInformation( new ContainerInformation(
1961 widgetInfo.widget,
1962 containerDef->visibilityExpression().enabled() ? containerDef->visibilityExpression().data() : QgsExpression(),
1963 containerDef->collapsed(),
1964 containerDef->collapsedExpression().enabled() ? containerDef->collapsedExpression().data() : QgsExpression()
1965 ) );
1966 }
1967 column += 2;
1968 break;
1969 }
1970
1972 {
1973 if ( !tabWidget )
1974 {
1975 tabWidget = new QgsTabWidget();
1976 layout->addWidget( tabWidget, row, column, 1, 2 );
1977 column += 2;
1978 }
1979
1980 QWidget *tabPage = new QWidget( tabWidget );
1981 tabWidget->addTab( tabPage, widgDef->name() );
1982 tabWidget->setTabStyle( tabWidget->tabBar()->count() - 1, widgDef->labelStyle() );
1983
1984 if ( containerDef->visibilityExpression().enabled() )
1985 {
1986 registerContainerInformation( new ContainerInformation( tabWidget, tabPage, containerDef->visibilityExpression().data() ) );
1987 }
1988 QGridLayout *tabPageLayout = new QGridLayout();
1989 tabPage->setLayout( tabPageLayout );
1990
1991 WidgetInfo widgetInfo = createWidgetFromDef( widgDef, tabPage, mLayer, mContext );
1992 if ( widgetInfo.expandingNeeded )
1993 {
1994 addSpacer = false;
1995 widgetInfo.widget->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Expanding );
1996 }
1997 tabPageLayout->addWidget( widgetInfo.widget );
1998 break;
1999 }
2000 }
2001 }
2002 else if ( widgDef->type() == Qgis::AttributeEditorType::Relation )
2003 {
2004 hasRootFields = true;
2005 tabWidget = nullptr;
2006 WidgetInfo widgetInfo = createWidgetFromDef( widgDef, container, mLayer, mContext );
2007 QgsCollapsibleGroupBox *collapsibleGroupBox = new QgsCollapsibleGroupBox();
2008
2009 if ( widgetInfo.showLabel )
2010 {
2011 if ( widgetInfo.labelStyle.overrideColor && widgetInfo.labelStyle.color.isValid() )
2012 {
2013 collapsibleGroupBox->setStyleSheet( u"QGroupBox::title { color: %1; }"_s.arg( widgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
2014 }
2015
2016 if ( widgetInfo.labelStyle.overrideFont )
2017 {
2018 collapsibleGroupBox->setFont( widgetInfo.labelStyle.font );
2019 }
2020
2021 collapsibleGroupBox->setTitle( widgetInfo.labelText );
2022 }
2023
2024 QVBoxLayout *collapsibleGroupBoxLayout = new QVBoxLayout();
2025 collapsibleGroupBoxLayout->addWidget( widgetInfo.widget );
2026 collapsibleGroupBox->setLayout( collapsibleGroupBoxLayout );
2027
2028 QVBoxLayout *c = new QVBoxLayout();
2029 c->addWidget( collapsibleGroupBox );
2030 layout->addLayout( c, row, column, 1, 2 );
2031
2032 if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
2033 layout->setRowStretch( row, widgDef->verticalStretch() );
2034 if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
2035 layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
2036
2037 column += 2;
2038
2039 // we consider all relation editors should be expanding
2040 addSpacer = false;
2041 }
2042 else
2043 {
2044 hasRootFields = true;
2045 tabWidget = nullptr;
2046 WidgetInfo widgetInfo = createWidgetFromDef( widgDef, container, mLayer, mContext );
2047 QWidget *labelWidget = new QWidget(); //to group label and comment icon in one widget
2048 QLabel *label = new QLabel( widgetInfo.labelText, labelWidget );
2049
2050 if ( widgetInfo.labelStyle.overrideColor )
2051 {
2052 if ( widgetInfo.labelStyle.color.isValid() )
2053 {
2054 label->setStyleSheet( u"QLabel { color: %1; }"_s.arg( widgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
2055 }
2056 }
2057
2058 if ( widgetInfo.labelStyle.overrideFont )
2059 {
2060 label->setFont( widgetInfo.labelStyle.font );
2061 }
2062
2063 labelWidget->setToolTip( widgetInfo.toolTip );
2064 if ( columnCount > 1 && !widgetInfo.labelOnTop )
2065 {
2066 label->setAlignment( Qt::AlignRight | Qt::AlignVCenter );
2067 }
2068
2069 label->setBuddy( widgetInfo.widget );
2070
2071 // Indicator button when a comment is available for the field
2072 QToolButton *commentInfoButton = createCommentInfoButton( labelWidget );
2073 // Only visible when there is a comment available
2074 if ( !widgetInfo.hint.isEmpty() )
2075 commentInfoButton->setVisible( true );
2076
2077 // If at least one expanding widget is present do not add a spacer
2078 if ( widgetInfo.widget
2079 && widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Fixed
2080 && widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Maximum
2081 && widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Preferred )
2082 addSpacer = false;
2083
2084 QHBoxLayout *labelLayout = new QHBoxLayout( labelWidget );
2085 labelLayout->addWidget( label );
2086 labelLayout->addWidget( commentInfoButton );
2087 labelLayout->setContentsMargins( 0, 0, 0, 0 );
2088
2089 if ( !widgetInfo.showLabel )
2090 {
2091 QVBoxLayout *c = new QVBoxLayout();
2092 label->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Fixed );
2093 c->addWidget( widgetInfo.widget );
2094 layout->addLayout( c, row, column, 1, 2 );
2095
2096 if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
2097 {
2098 layout->setRowStretch( row, widgDef->verticalStretch() );
2099 addSpacer = false;
2100 }
2101 if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
2102 {
2103 layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
2104 }
2105
2106 column += 2;
2107 }
2108 else if ( widgetInfo.labelOnTop )
2109 {
2110 QVBoxLayout *c = new QVBoxLayout();
2111 label->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Fixed );
2112 c->addWidget( labelWidget );
2113 c->addWidget( widgetInfo.widget );
2114 layout->addLayout( c, row, column, 1, 2 );
2115
2116 if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
2117 {
2118 layout->setRowStretch( row, widgDef->verticalStretch() );
2119 addSpacer = false;
2120 }
2121 if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
2122 {
2123 layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
2124 }
2125
2126 column += 2;
2127 }
2128 else
2129 {
2130 const int widgetColumn = column + 1;
2131 layout->addWidget( labelWidget, row, column++ );
2132 layout->addWidget( widgetInfo.widget, row, column++ );
2133
2134 if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
2135 {
2136 layout->setRowStretch( row, widgDef->verticalStretch() );
2137 addSpacer = false;
2138 }
2139 if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( widgetColumn ) )
2140 {
2141 layout->setColumnStretch( widgetColumn, widgDef->horizontalStretch() );
2142 }
2143 }
2144
2145 // Alias DD overrides
2146 if ( widgDef->type() == Qgis::AttributeEditorType::Field )
2147 {
2148 const QgsAttributeEditorField *fieldElement { static_cast<QgsAttributeEditorField *>( widgDef ) };
2149 const int fieldIdx = fieldElement->idx();
2150 if ( fieldIdx >= 0 && fieldIdx < mLayer->fields().count() )
2151 {
2152 const QString fieldName { mLayer->fields().at( fieldIdx ).name() };
2153 label->setObjectName( fieldName );
2154
2155 if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::Alias ) )
2156 {
2157 const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::Alias ) };
2158 if ( property.isActive() )
2159 {
2160 mLabelDataDefinedProperties[label] = property;
2161 }
2162 }
2163 labelWidget->setObjectName( fieldName );
2164 if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::CustomComment ) )
2165 {
2166 const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::CustomComment ) };
2167 if ( property.isActive() )
2168 {
2169 mCustomCommentDataDefinedProperties[labelWidget] = property;
2170 commentInfoButton->setVisible( true );
2171 }
2172 }
2173 if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::Editable ) )
2174 {
2175 const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::Editable ) };
2176 if ( property.isActive() )
2177 {
2178 mEditableDataDefinedProperties[widgetInfo.widget] = property;
2179 }
2180 }
2181 }
2182 }
2183 }
2184
2185 if ( column >= columnCount * 2 )
2186 {
2187 column = 0;
2188 row += 1;
2189 }
2190 }
2191
2192 if ( hasRootFields && addSpacer )
2193 {
2194 QSpacerItem *spacerItem = new QSpacerItem( 20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding );
2195 layout->addItem( spacerItem, row, 0 );
2196 layout->setRowStretch( row, 1 );
2197 }
2198
2199 formWidget = container;
2200 }
2201
2202 // Autogenerate Layout
2203 // If there is still no layout loaded (defined as autogenerate or other methods failed)
2204 mIconMap.clear();
2205
2206 if ( !formWidget )
2207 {
2208 formWidget = new QWidget( this );
2209 QGridLayout *gridLayout = new QGridLayout( formWidget );
2210 formWidget->setLayout( gridLayout );
2211
2212 if ( mContext.formMode() != QgsAttributeEditorContext::Embed )
2213 {
2214 // put the form into a scroll area to nicely handle cases with lots of attributes
2215 QgsScrollArea *scrollArea = new QgsScrollArea( this );
2216 scrollArea->setWidget( formWidget );
2217 scrollArea->setWidgetResizable( true );
2218 scrollArea->setFrameShape( QFrame::NoFrame );
2219 scrollArea->setFrameShadow( QFrame::Plain );
2220 scrollArea->setFocusProxy( this );
2221 layout->addWidget( scrollArea );
2222 }
2223 else
2224 {
2225 layout->addWidget( formWidget );
2226 }
2227
2228 int row = 0;
2229
2230 const QgsFields fields = mLayer->fields();
2231
2232 // Collect non-first fields of composite foreign keys — these should be
2233 // hidden since the RelationReference widget on the first field manages
2234 // all composite key values internally
2235 QSet<int> compositeHiddenFields;
2236 const QList<QgsRelation> referencingRelations = QgsProject::instance()->relationManager()->referencingRelations( mLayer );
2237 for ( const QgsRelation &rel : referencingRelations )
2238 {
2239 if ( rel.type() != Qgis::RelationshipType::Normal )
2240 continue;
2241
2242 const QList<QgsRelation::FieldPair> fieldPairs = rel.fieldPairs();
2243 if ( fieldPairs.size() > 1 )
2244 {
2245 for ( int i = 1; i < fieldPairs.size(); i++ )
2246 {
2247 const int idx = fields.lookupField( fieldPairs.at( i ).referencingField() );
2248 if ( idx >= 0 )
2249 compositeHiddenFields.insert( idx );
2250 }
2251 }
2252 }
2253
2254 for ( const QgsField &field : fields )
2255 {
2256 const QString fieldName = field.name();
2257 int idx = fields.lookupField( fieldName );
2258 if ( idx < 0 )
2259 continue;
2260
2261 if ( compositeHiddenFields.contains( idx ) )
2262 continue;
2263
2264 //show attribute alias if available
2265 QString labelText = mLayer->attributeDisplayName( idx );
2266 labelText.replace( '&', "&&"_L1 ); // need to escape '&' or they'll be replace by _ in the label text
2267
2268 const QgsEditorWidgetSetup widgetSetup = QgsGui::editorWidgetRegistry()->findBest( mLayer, field.name() );
2269
2270 if ( widgetSetup.type() == "Hidden"_L1 )
2271 continue;
2272
2273 bool labelOnTop = mLayer->editFormConfig().labelOnTop( idx );
2274
2275 // This will also create the widget
2276 QWidget *labelWidget = new QWidget(); //to group label and comment icon in one widget
2277 QLabel *label = new QLabel( labelText, labelWidget );
2278 label->setObjectName( fieldName );
2279
2280 labelWidget->setToolTip( QgsFieldModel::fieldToolTipExtended( field, mLayer ) );
2281
2282 // Indicator button when a comment is available for the field
2283 QToolButton *commentInfoButton = createCommentInfoButton( labelWidget );
2284 // Only visible when there is a comment available (not visible when custom comment is an empty string)
2285 if ( !field.customComment().isEmpty() || ( field.customComment().isNull() && !field.comment().isEmpty() ) )
2286 commentInfoButton->setVisible( true );
2287
2288 QSvgWidget *i = new QSvgWidget();
2289 i->setFixedSize( 18, 18 );
2290
2291 if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::Alias ) )
2292 {
2293 const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::Alias ) };
2294 if ( property.isActive() )
2295 {
2296 mLabelDataDefinedProperties[label] = property;
2297 }
2298 }
2299 labelWidget->setObjectName( fieldName );
2300 if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::CustomComment ) )
2301 {
2302 const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::CustomComment ) };
2303 if ( property.isActive() )
2304 {
2305 mCustomCommentDataDefinedProperties[labelWidget] = property;
2306 commentInfoButton->setVisible( true );
2307 }
2308 }
2309
2310 QgsEditorWidgetWrapper *eww = QgsGui::editorWidgetRegistry()->create( widgetSetup.type(), mLayer, idx, widgetSetup.config(), nullptr, this, mContext );
2311
2312 QWidget *w = nullptr;
2313 if ( eww )
2314 {
2315 QgsAttributeFormEditorWidget *formWidget = new QgsAttributeFormEditorWidget( eww, widgetSetup.type(), this );
2316 w = formWidget;
2317 mFormEditorWidgets.insert( idx, formWidget );
2318 mFormWidgets.append( formWidget );
2319
2320 const Qgis::AttributeFormReuseLastValuePolicy reusePolicy = mLayer->editFormConfig().reuseLastValuePolicy( idx );
2322 {
2323 bool remember = reusePolicy == Qgis::AttributeFormReuseLastValuePolicy::AllowedDefaultOn;
2324 const QVariant rememberLastUsedValuesVariant = mLayer->property( "AttributeFormRememberLastUsedValues" );
2325 QMap<int, bool> rememberLastUsedValues = rememberLastUsedValuesVariant.value<QMap<int, bool>>();
2326 if ( rememberLastUsedValues.contains( idx ) )
2327 {
2328 remember = rememberLastUsedValues[idx];
2329 }
2330
2331 formWidget->setRememberLastValue( remember );
2332 connect( formWidget, &QgsAttributeFormEditorWidget::rememberLastValueChanged, this, [this]( int idx, bool remember ) {
2333 const QVariant rememberLastUsedValuesVariant = mLayer->property( "AttributeFormRememberLastUsedValues" );
2334 QMap<int, bool> rememberLastUsedValues = rememberLastUsedValuesVariant.value<QMap<int, bool>>();
2335 rememberLastUsedValues[idx] = remember;
2336 mLayer->setProperty( "AttributeFormRememberLastUsedValues", QVariant::fromValue<QMap<int, bool>>( rememberLastUsedValues ) );
2337 } );
2338 }
2339
2340 formWidget->createSearchWidgetWrappers( mContext );
2341
2342 label->setBuddy( eww->widget() );
2343
2344 if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::Editable ) )
2345 {
2346 const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::Editable ) };
2347 if ( property.isActive() )
2348 {
2349 mEditableDataDefinedProperties[formWidget] = property;
2350 }
2351 }
2352 }
2353 else
2354 {
2355 w = new QLabel( u"<p style=\"color: red; font-style: italic;\">%1</p>"_s.arg( tr( "Failed to create widget with type '%1'" ).arg( widgetSetup.type() ) ) );
2356 }
2357
2358
2359 if ( w )
2360 w->setObjectName( field.name() );
2361
2362 if ( eww )
2363 {
2364 mWidgets.append( eww );
2365 mIconMap[eww->widget()] = i;
2366 }
2367
2368 QHBoxLayout *labelLayout = new QHBoxLayout( labelWidget );
2369 labelLayout->addWidget( label );
2370 labelLayout->addWidget( commentInfoButton );
2371 labelLayout->setContentsMargins( 0, 0, 0, 0 );
2372
2373 if ( labelOnTop )
2374 {
2375 gridLayout->addWidget( labelWidget, row++, 0, 1, 2 );
2376 gridLayout->addWidget( w, row++, 0, 1, 2 );
2377 gridLayout->addWidget( i, row++, 0, 1, 2 );
2378 }
2379 else
2380 {
2381 int widgetColumn = 0;
2382 gridLayout->addWidget( labelWidget, row, widgetColumn++ );
2383 gridLayout->addWidget( w, row, widgetColumn++ );
2384 gridLayout->addWidget( i, row++, widgetColumn++ );
2385 }
2386 }
2387
2388 const QList<QgsRelation> relations = QgsProject::instance()->relationManager()->referencedRelations( mLayer );
2389 for ( const QgsRelation &rel : relations )
2390 {
2391 QgsRelationWidgetWrapper *rww = setupRelationWidgetWrapper( u"relation_editor"_s, rel, mContext );
2392
2393 QgsAttributeFormRelationEditorWidget *formWidget = new QgsAttributeFormRelationEditorWidget( rww, this );
2394 formWidget->createSearchWidgetWrappers( mContext );
2395
2396 QgsCollapsibleGroupBox *collapsibleGroupBox = new QgsCollapsibleGroupBox( rel.name() );
2397 QVBoxLayout *collapsibleGroupBoxLayout = new QVBoxLayout();
2398 collapsibleGroupBoxLayout->addWidget( formWidget );
2399 collapsibleGroupBox->setLayout( collapsibleGroupBoxLayout );
2400
2401 gridLayout->addWidget( collapsibleGroupBox, row++, 0, 1, 2 );
2402
2403 mWidgets.append( rww );
2404 mFormWidgets.append( formWidget );
2405 }
2406
2407 if ( QgsProject::instance()->relationManager()->referencedRelations( mLayer ).isEmpty() )
2408 {
2409 QSpacerItem *spacerItem = new QSpacerItem( 20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding );
2410 gridLayout->addItem( spacerItem, row, 0 );
2411 gridLayout->setRowStretch( row, 1 );
2412 row++;
2413 }
2414 }
2415
2416 // Prepare value dependencies
2417 updateFieldDependencies();
2418
2419 if ( !mButtonBox )
2420 {
2421 mButtonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel );
2422 mButtonBox->setObjectName( u"buttonBox"_s );
2423 layout->addWidget( mButtonBox, layout->rowCount(), 0, 1, layout->columnCount() );
2424 }
2425 mButtonBox->setVisible( buttonBoxVisible );
2426
2427 if ( !mSearchButtonBox )
2428 {
2429 mSearchButtonBox = new QWidget();
2430 QHBoxLayout *boxLayout = new QHBoxLayout();
2431 boxLayout->setContentsMargins( 0, 0, 0, 0 );
2432 mSearchButtonBox->setLayout( boxLayout );
2433 mSearchButtonBox->setObjectName( u"searchButtonBox"_s );
2434
2435 QPushButton *clearButton = new QPushButton( tr( "&Reset Form" ), mSearchButtonBox );
2436 connect( clearButton, &QPushButton::clicked, this, &QgsAttributeForm::resetSearch );
2437 boxLayout->addWidget( clearButton );
2438 boxLayout->addStretch( 1 );
2439
2440 QPushButton *flashButton = new QPushButton();
2441 flashButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
2442 flashButton->setText( tr( "&Flash Features" ) );
2443 connect( flashButton, &QToolButton::clicked, this, &QgsAttributeForm::searchFlash );
2444 boxLayout->addWidget( flashButton );
2445
2446 QPushButton *openAttributeTableButton = new QPushButton();
2447 openAttributeTableButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
2448 openAttributeTableButton->setText( tr( "Show in &Table" ) );
2449 openAttributeTableButton->setToolTip( tr( "Open the attribute table editor with the filtered features" ) );
2450 connect( openAttributeTableButton, &QToolButton::clicked, this, [this] { emit openFilteredFeaturesAttributeTable( createFilterExpression() ); } );
2451 boxLayout->addWidget( openAttributeTableButton );
2452
2453 QPushButton *zoomButton = new QPushButton();
2454 zoomButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
2455 zoomButton->setText( tr( "&Zoom to Features" ) );
2456 connect( zoomButton, &QToolButton::clicked, this, &QgsAttributeForm::searchZoomTo );
2457 boxLayout->addWidget( zoomButton );
2458
2459 QToolButton *selectButton = new QToolButton();
2460 selectButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
2461 selectButton->setText( tr( "&Select Features" ) );
2462 selectButton->setIcon( QgsApplication::getThemeIcon( u"/mIconFormSelect.svg"_s ) );
2463 selectButton->setPopupMode( QToolButton::MenuButtonPopup );
2464 selectButton->setToolButtonStyle( Qt::ToolButtonTextBesideIcon );
2465 connect( selectButton, &QToolButton::clicked, this, &QgsAttributeForm::searchSetSelection );
2466 QMenu *selectMenu = new QMenu( selectButton );
2467 QAction *selectAction = new QAction( tr( "Select Features" ), selectMenu );
2468 selectAction->setIcon( QgsApplication::getThemeIcon( u"/mIconFormSelect.svg"_s ) );
2469 connect( selectAction, &QAction::triggered, this, &QgsAttributeForm::searchSetSelection );
2470 selectMenu->addAction( selectAction );
2471 QAction *addSelectAction = new QAction( tr( "Add to Current Selection" ), selectMenu );
2472 addSelectAction->setIcon( QgsApplication::getThemeIcon( u"/mIconSelectAdd.svg"_s ) );
2473 connect( addSelectAction, &QAction::triggered, this, &QgsAttributeForm::searchAddToSelection );
2474 selectMenu->addAction( addSelectAction );
2475 QAction *deselectAction = new QAction( tr( "Remove from Current Selection" ), selectMenu );
2476 deselectAction->setIcon( QgsApplication::getThemeIcon( u"/mIconSelectRemove.svg"_s ) );
2477 connect( deselectAction, &QAction::triggered, this, &QgsAttributeForm::searchRemoveFromSelection );
2478 selectMenu->addAction( deselectAction );
2479 QAction *filterSelectAction = new QAction( tr( "Filter Current Selection" ), selectMenu );
2480 filterSelectAction->setIcon( QgsApplication::getThemeIcon( u"/mIconSelectIntersect.svg"_s ) );
2481 connect( filterSelectAction, &QAction::triggered, this, &QgsAttributeForm::searchIntersectSelection );
2482 selectMenu->addAction( filterSelectAction );
2483 selectButton->setMenu( selectMenu );
2484 boxLayout->addWidget( selectButton );
2485
2486 if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
2487 {
2488 QToolButton *filterButton = new QToolButton();
2489 filterButton->setText( tr( "Filter Features" ) );
2490 filterButton->setPopupMode( QToolButton::MenuButtonPopup );
2491 filterButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
2492 connect( filterButton, &QToolButton::clicked, this, &QgsAttributeForm::filterTriggered );
2493 QMenu *filterMenu = new QMenu( filterButton );
2494 QAction *filterAndAction = new QAction( tr( "Filter Within (\"AND\")" ), filterMenu );
2495 connect( filterAndAction, &QAction::triggered, this, &QgsAttributeForm::filterAndTriggered );
2496 filterMenu->addAction( filterAndAction );
2497 QAction *filterOrAction = new QAction( tr( "Extend Filter (\"OR\")" ), filterMenu );
2498 connect( filterOrAction, &QAction::triggered, this, &QgsAttributeForm::filterOrTriggered );
2499 filterMenu->addAction( filterOrAction );
2500 filterButton->setMenu( filterMenu );
2501 boxLayout->addWidget( filterButton );
2502 }
2503 else
2504 {
2505 QPushButton *closeButton = new QPushButton( tr( "Close" ), mSearchButtonBox );
2506 connect( closeButton, &QPushButton::clicked, this, &QgsAttributeForm::closed );
2507 closeButton->setShortcut( Qt::Key_Escape );
2508 boxLayout->addWidget( closeButton );
2509 }
2510
2511 layout->addWidget( mSearchButtonBox, layout->rowCount(), 0, 1, layout->columnCount() );
2512 }
2513 mSearchButtonBox->setVisible( mMode == QgsAttributeEditorContext::SearchMode );
2514
2515 afterWidgetInit();
2516
2517 connect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsAttributeForm::save );
2518 connect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsAttributeForm::resetValues );
2519
2520 connect( mLayer, &QgsVectorLayer::editingStarted, this, &QgsAttributeForm::synchronizeState );
2521 connect( mLayer, &QgsVectorLayer::editingStopped, this, &QgsAttributeForm::synchronizeState );
2522
2523 // This triggers a refresh of the form widget and gives a chance to re-format the
2524 // value to those widgets that have a different representation when in edit mode
2527
2528
2529 const auto constMInterfaces = mInterfaces;
2530 for ( QgsAttributeFormInterface *iface : constMInterfaces )
2531 {
2532 iface->initForm();
2533 }
2534
2535 if ( mContext.formMode() == QgsAttributeEditorContext::Embed || mMode == QgsAttributeEditorContext::SearchMode )
2536 {
2537 hideButtonBox();
2538 }
2539
2540 QApplication::restoreOverrideCursor();
2541}
2542
2543void QgsAttributeForm::cleanPython()
2544{
2545 if ( !mPyFormVarName.isNull() )
2546 {
2547 QString expr = u"if '%1' in locals(): del %1\n"_s.arg( mPyFormVarName );
2548 QgsPythonRunner::run( expr );
2549 }
2550}
2551
2552void QgsAttributeForm::initPython()
2553{
2554 cleanPython();
2555
2556 // Init Python, if init function is not empty and the combo indicates
2557 // the source for the function code
2558 if ( !mLayer->editFormConfig().initFunction().isEmpty() && mLayer->editFormConfig().initCodeSource() != Qgis::AttributeFormPythonInitCodeSource::NoSource )
2559 {
2561 if ( !allowed )
2562 {
2563 mMessageBar->pushMessage( tr( "Security warning" ), tr( "The attribute form contains an embedded script which has been denied execution." ), Qgis::MessageLevel::Warning );
2564 return;
2565 }
2566
2567 QString initFunction = mLayer->editFormConfig().initFunction();
2568 QString initFilePath = mLayer->editFormConfig().initFilePath();
2569 QString initCode;
2570
2571 switch ( mLayer->editFormConfig().initCodeSource() )
2572 {
2574 if ( !initFilePath.isEmpty() )
2575 {
2576 QFile *inputFile = QgsApplication::networkContentFetcherRegistry()->localFile( initFilePath );
2577
2578 if ( inputFile && inputFile->open( QFile::ReadOnly ) )
2579 {
2580 // Read it into a string
2581 QTextStream inf( inputFile );
2582 initCode = inf.readAll();
2583 inputFile->close();
2584 }
2585 else // The file couldn't be opened
2586 {
2587 QgsLogger::warning( u"The external python file path %1 could not be opened!"_s.arg( initFilePath ) );
2588 }
2589 }
2590 else
2591 {
2592 QgsLogger::warning( u"The external python file path is empty!"_s );
2593 }
2594 break;
2595
2597 initCode = mLayer->editFormConfig().initCode();
2598 if ( initCode.isEmpty() )
2599 {
2600 QgsLogger::warning( u"The python code provided in the dialog is empty!"_s );
2601 }
2602 break;
2603
2606 // Nothing to do: the function code should be already in the environment
2607 break;
2608 }
2609
2610 // If we have a function code, run it
2611 if ( !initCode.isEmpty() )
2612 {
2614 {
2615 QgsPythonRunner::run( initCode );
2616 }
2617 else
2618 {
2619 mMessageBar->pushMessage( QString(), tr( "Python macro could not be run due to missing permissions." ), Qgis::MessageLevel::Warning );
2620 }
2621 }
2622
2623 QgsPythonRunner::run( u"import inspect"_s );
2624 QString numArgs;
2625
2626 // Check for eval result
2627 if ( QgsPythonRunner::eval( u"len(inspect.getfullargspec(%1)[0])"_s.arg( initFunction ), numArgs ) )
2628 {
2629 static int sFormId = 0;
2630 mPyFormVarName = u"_qgis_featureform_%1_%2"_s.arg( mFormNr ).arg( sFormId++ );
2631
2632 QString form = u"%1 = sip.wrapinstance( %2, qgis.gui.QgsAttributeForm )"_s.arg( mPyFormVarName ).arg( ( quint64 ) this );
2633
2634 QgsPythonRunner::run( form );
2635
2636 QgsDebugMsgLevel( u"running featureForm init: %1"_s.arg( mPyFormVarName ), 2 );
2637
2638 // Legacy
2639 if ( numArgs == "3"_L1 )
2640 {
2641 addInterface( new QgsAttributeFormLegacyInterface( initFunction, mPyFormVarName, this ) );
2642 }
2643 else
2644 {
2645 // If we get here, it means that the function doesn't accept three arguments
2646 QMessageBox msgBox;
2647 msgBox.setText(
2648 tr( "The python init function (<code>%1</code>) does not accept three arguments as expected!<br>Please check the function name in the <b>Fields</b> tab of the layer properties." ).arg( initFunction )
2649 );
2650 msgBox.exec();
2651#if 0
2652 QString expr = QString( "%1(%2)" )
2653 .arg( mLayer->editFormInit() )
2654 .arg( mPyFormVarName );
2655 QgsAttributeFormInterface *iface = QgsPythonRunner::evalToSipObject<QgsAttributeFormInterface *>( expr, "QgsAttributeFormInterface" );
2656 if ( iface )
2657 addInterface( iface );
2658#endif
2659 }
2660 }
2661 else
2662 {
2663 // If we get here, it means that inspect couldn't find the function
2664 QMessageBox msgBox;
2665 msgBox.setText( tr( "The python init function (<code>%1</code>) could not be found!<br>Please check the function name in the <b>Fields</b> tab of the layer properties." ).arg( initFunction ) );
2666 msgBox.exec();
2667 }
2668 }
2669}
2670
2671QgsAttributeForm::WidgetInfo QgsAttributeForm::createWidgetFromDef( const QgsAttributeEditorElement *widgetDef, QWidget *parent, QgsVectorLayer *vl, QgsAttributeEditorContext &context )
2672{
2673 WidgetInfo newWidgetInfo;
2674
2675 newWidgetInfo.labelStyle = widgetDef->labelStyle();
2676
2677 switch ( widgetDef->type() )
2678 {
2680 {
2681 const QgsAttributeEditorAction *elementDef = dynamic_cast<const QgsAttributeEditorAction *>( widgetDef );
2682 if ( !elementDef )
2683 break;
2684
2685 QgsActionWidgetWrapper *actionWrapper = new QgsActionWidgetWrapper( mLayer, nullptr, this, mMessageBar );
2686 actionWrapper->setAction( elementDef->action( vl ) );
2687 context.setAttributeFormMode( mMode );
2688 actionWrapper->setContext( context );
2689 mWidgets.append( actionWrapper );
2690 newWidgetInfo.widget = actionWrapper->widget();
2691 newWidgetInfo.showLabel = false;
2692
2693 break;
2694 }
2695
2697 {
2698 const QgsAttributeEditorField *fieldDef = dynamic_cast<const QgsAttributeEditorField *>( widgetDef );
2699 if ( !fieldDef )
2700 break;
2701
2702 const QgsFields fields = vl->fields();
2703 int fldIdx = fields.lookupField( fieldDef->name() );
2704 if ( fldIdx < fields.count() && fldIdx >= 0 )
2705 {
2706 const QgsEditorWidgetSetup widgetSetup = QgsGui::editorWidgetRegistry()->findBest( mLayer, fieldDef->name() );
2707
2708 QgsEditorWidgetWrapper *eww = QgsGui::editorWidgetRegistry()->create( widgetSetup.type(), mLayer, fldIdx, widgetSetup.config(), nullptr, this, mContext );
2709 QgsAttributeFormEditorWidget *formWidget = new QgsAttributeFormEditorWidget( eww, widgetSetup.type(), this );
2710 mFormEditorWidgets.insert( fldIdx, formWidget );
2711 mFormWidgets.append( formWidget );
2712
2713 const Qgis::AttributeFormReuseLastValuePolicy reusePolicy = mLayer->editFormConfig().reuseLastValuePolicy( fldIdx );
2715 {
2716 bool remember = reusePolicy == Qgis::AttributeFormReuseLastValuePolicy::AllowedDefaultOn;
2717 const QVariant rememberLastUsedValuesVariant = mLayer->property( "AttributeFormRememberLastUsedValues" );
2718 QMap<int, bool> rememberLastUsedValues = rememberLastUsedValuesVariant.value<QMap<int, bool>>();
2719 if ( rememberLastUsedValues.contains( fldIdx ) )
2720 {
2721 remember = rememberLastUsedValues[fldIdx];
2722 }
2723 formWidget->setRememberLastValue( remember );
2724 connect( formWidget, &QgsAttributeFormEditorWidget::rememberLastValueChanged, this, [this]( int idx, bool remember ) {
2725 const QVariant rememberLastUsedValuesVariant = mLayer->property( "AttributeFormRememberLastUsedValues" );
2726 QMap<int, bool> rememberLastUsedValues = rememberLastUsedValuesVariant.value<QMap<int, bool>>();
2727 rememberLastUsedValues[idx] = remember;
2728 mLayer->setProperty( "AttributeFormRememberLastUsedValues", QVariant::fromValue<QMap<int, bool>>( rememberLastUsedValues ) );
2729 } );
2730 }
2731
2732 formWidget->createSearchWidgetWrappers( mContext );
2733
2734 newWidgetInfo.widget = formWidget;
2735 mWidgets.append( eww );
2736
2737 newWidgetInfo.widget->setObjectName( fields.at( fldIdx ).name() );
2738 const QString customComment = fields.at( fldIdx ).customComment();
2739 newWidgetInfo.hint = customComment.isNull() ? fields.at( fldIdx ).comment() : customComment;
2740
2741 newWidgetInfo.labelOnTop = mLayer->editFormConfig().labelOnTop( fldIdx );
2742 newWidgetInfo.labelText = mLayer->attributeDisplayName( fldIdx );
2743 newWidgetInfo.labelText.replace( '&', "&&"_L1 ); // need to escape '&' or they'll be replace by _ in the label text
2744
2745 newWidgetInfo.toolTip = ( QgsFieldModel::fieldToolTipExtended( fields.at( fldIdx ), mLayer ) );
2746 }
2747 else
2748 {
2749 QgsDebugError( u"Attribute form references unknown field '%1' on layer '%2' - skipping widget"_s.arg( fieldDef->name(), mLayer ? mLayer->name() : QString() ) );
2750 newWidgetInfo.labelOnTop = false;
2751 newWidgetInfo.labelText = fieldDef->name();
2752 newWidgetInfo.labelText.replace( '&', "&&"_L1 );
2753 newWidgetInfo.toolTip.clear();
2754 }
2755
2756 newWidgetInfo.showLabel = widgetDef->showLabel();
2757
2758 break;
2759 }
2760
2762 {
2763 const QgsAttributeEditorRelation *relDef = static_cast<const QgsAttributeEditorRelation *>( widgetDef );
2764
2765 QgsRelationWidgetWrapper *rww = setupRelationWidgetWrapper( relDef->relationWidgetTypeId(), relDef->relation(), context );
2766
2767 QgsAttributeFormRelationEditorWidget *formWidget = new QgsAttributeFormRelationEditorWidget( rww, this );
2768 formWidget->createSearchWidgetWrappers( mContext );
2769
2770 // This needs to be after QgsAttributeFormRelationEditorWidget creation, because the widget
2771 // does not exists yet until QgsAttributeFormRelationEditorWidget is created and the setters
2772 // below directly alter the widget and check for it.
2774 rww->setNmRelationId( relDef->nmRelationId() );
2776
2777 mWidgets.append( rww );
2778 mFormWidgets.append( formWidget );
2779
2780 newWidgetInfo.widget = formWidget;
2781 newWidgetInfo.showLabel = relDef->showLabel();
2782 newWidgetInfo.labelText = relDef->label();
2783 if ( newWidgetInfo.labelText.isEmpty() )
2784 newWidgetInfo.labelText = rww->relation().name();
2785 newWidgetInfo.labelOnTop = true;
2786 break;
2787 }
2788
2790 {
2791 const QgsAttributeEditorContainer *container = dynamic_cast<const QgsAttributeEditorContainer *>( widgetDef );
2792 if ( !container )
2793 break;
2794
2795 int columnCount = container->columnCount();
2796
2797 if ( columnCount <= 0 )
2798 columnCount = 1;
2799
2800 QString widgetName;
2801 QWidget *myContainer = nullptr;
2802 bool removeLayoutMargin = false;
2803 switch ( container->type() )
2804 {
2806 {
2807 QgsCollapsibleGroupBoxBasic *groupBox = new QgsCollapsibleGroupBoxBasic();
2808 widgetName = u"QGroupBox"_s;
2809 if ( container->showLabel() )
2810 {
2811 groupBox->setTitle( container->name() );
2812 if ( newWidgetInfo.labelStyle.overrideColor )
2813 {
2814 if ( newWidgetInfo.labelStyle.color.isValid() )
2815 {
2816 groupBox->setStyleSheet( u"QGroupBox::title { color: %1; }"_s.arg( newWidgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
2817 }
2818 }
2819 if ( newWidgetInfo.labelStyle.overrideFont )
2820 {
2821 groupBox->setFont( newWidgetInfo.labelStyle.font );
2822 }
2823 }
2824 myContainer = groupBox;
2825 newWidgetInfo.widget = myContainer;
2826 groupBox->setCollapsed( container->collapsed() );
2827 break;
2828 }
2829
2831 {
2832 QWidget *rowWidget = new QWidget();
2833 widgetName = u"Row"_s;
2834 myContainer = rowWidget;
2835 newWidgetInfo.widget = myContainer;
2836 removeLayoutMargin = true;
2837 columnCount = container->children().size();
2838 break;
2839 }
2840
2842 {
2843 myContainer = new QWidget();
2844
2845 QgsScrollArea *scrollArea = new QgsScrollArea( parent );
2846
2847 scrollArea->setWidget( myContainer );
2848 scrollArea->setWidgetResizable( true );
2849 scrollArea->setFrameShape( QFrame::NoFrame );
2850 widgetName = u"QScrollArea QWidget"_s;
2851
2852 newWidgetInfo.widget = scrollArea;
2853 break;
2854 }
2855 }
2856
2857 if ( container->backgroundColor().isValid() )
2858 {
2859 QString style { u"background-color: %1;"_s.arg( container->backgroundColor().name() ) };
2860 newWidgetInfo.widget->setStyleSheet( style );
2861 }
2862
2863 QGridLayout *gbLayout = new QGridLayout();
2864 if ( removeLayoutMargin )
2865 gbLayout->setContentsMargins( 0, 0, 0, 0 );
2866 myContainer->setLayout( gbLayout );
2867
2868 int row = 0;
2869 int column = 0;
2870 bool addSpacer = true;
2871
2872 const QList<QgsAttributeEditorElement *> children = container->children();
2873
2874 for ( QgsAttributeEditorElement *childDef : children )
2875 {
2876 WidgetInfo widgetInfo = createWidgetFromDef( childDef, myContainer, vl, context );
2877
2878 if ( childDef->type() == Qgis::AttributeEditorType::Container )
2879 {
2880 QgsAttributeEditorContainer *containerDef = static_cast<QgsAttributeEditorContainer *>( childDef );
2881 if ( containerDef->visibilityExpression().enabled() || containerDef->collapsedExpression().enabled() )
2882 {
2883 registerContainerInformation( new ContainerInformation(
2884 widgetInfo.widget,
2885 containerDef->visibilityExpression().enabled() ? containerDef->visibilityExpression().data() : QgsExpression(),
2886 containerDef->collapsed(),
2887 containerDef->collapsedExpression().enabled() ? containerDef->collapsedExpression().data() : QgsExpression()
2888 ) );
2889 }
2890 if ( childDef->verticalStretch() == 0 )
2891 {
2892 if ( widgetInfo.expandingNeeded )
2893 {
2894 widgetInfo.widget->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Expanding );
2895 }
2896 else
2897 {
2898 widgetInfo.widget->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Maximum );
2899 }
2900 }
2901 }
2902
2903 // column containing the actual widget, not the label
2904 int widgetColumn = column;
2905
2906 if ( widgetInfo.labelText.isNull() || !widgetInfo.showLabel )
2907 {
2908 gbLayout->addWidget( widgetInfo.widget, row, column, 1, 2 );
2909 widgetColumn = column + 1;
2910 column += 2;
2911 }
2912 else
2913 {
2914 QWidget *labelWidget = new QWidget(); //to group label and comment icon in one widget
2915 QLabel *mypLabel = new QLabel( widgetInfo.labelText, labelWidget );
2916
2917 if ( widgetInfo.labelStyle.overrideColor )
2918 {
2919 if ( widgetInfo.labelStyle.color.isValid() )
2920 {
2921 mypLabel->setStyleSheet( u"QLabel { color: %1; }"_s.arg( widgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
2922 }
2923 }
2924
2925 if ( widgetInfo.labelStyle.overrideFont )
2926 {
2927 mypLabel->setFont( widgetInfo.labelStyle.font );
2928 }
2929
2930 // Indicator button when a comment is available for the field
2931 QToolButton *commentInfoButton = createCommentInfoButton( labelWidget );
2932 // Only visible when there is a comment available
2933 if ( !widgetInfo.hint.isEmpty() )
2934 commentInfoButton->setVisible( true );
2935
2936 // Alias DD overrides
2937 if ( childDef->type() == Qgis::AttributeEditorType::Field )
2938 {
2939 const QgsAttributeEditorField *fieldDef { static_cast<QgsAttributeEditorField *>( childDef ) };
2940 const QgsFields fields = vl->fields();
2941 const int fldIdx = fieldDef->idx();
2942 if ( fldIdx < fields.count() && fldIdx >= 0 )
2943 {
2944 const QString fieldName { fields.at( fldIdx ).name() };
2945 mypLabel->setObjectName( fieldName );
2946
2947 if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::Alias ) )
2948 {
2949 const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::Alias ) };
2950 if ( property.isActive() )
2951 {
2952 mLabelDataDefinedProperties[mypLabel] = property;
2953 }
2954 }
2955 if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::Editable ) )
2956 {
2957 const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::Editable ) };
2958 if ( property.isActive() )
2959 {
2960 mEditableDataDefinedProperties[widgetInfo.widget] = property;
2961 }
2962 }
2963 labelWidget->setObjectName( fieldName );
2964 if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::CustomComment ) )
2965 {
2966 const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::CustomComment ) };
2967 if ( property.isActive() )
2968 {
2969 mCustomCommentDataDefinedProperties[labelWidget] = property;
2970 }
2971 }
2972 }
2973 }
2974
2975 labelWidget->setToolTip( widgetInfo.toolTip );
2976 if ( columnCount > 1 && !widgetInfo.labelOnTop )
2977 {
2978 mypLabel->setAlignment( Qt::AlignRight | Qt::AlignVCenter );
2979 }
2980
2981 mypLabel->setBuddy( widgetInfo.widget );
2982
2983 QHBoxLayout *labelLayout = new QHBoxLayout( labelWidget );
2984 labelLayout->addWidget( mypLabel );
2985 labelLayout->addWidget( commentInfoButton );
2986 labelLayout->setContentsMargins( 0, 0, 0, 0 );
2987
2988 if ( widgetInfo.labelOnTop )
2989 {
2990 widgetColumn = column + 1;
2991 QVBoxLayout *c = new QVBoxLayout();
2992 mypLabel->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Fixed );
2993 c->addWidget( labelWidget );
2994 c->layout()->addWidget( widgetInfo.widget );
2995 gbLayout->addLayout( c, row, column, 1, 2 );
2996 column += 2;
2997 }
2998 else
2999 {
3000 widgetColumn = column + 1;
3001 gbLayout->addWidget( labelWidget, row, column++ );
3002 gbLayout->addWidget( widgetInfo.widget, row, column++ );
3003 }
3004 }
3005
3006 const int childHorizontalStretch = childDef->horizontalStretch();
3007 const int existingColumnStretch = gbLayout->columnStretch( widgetColumn );
3008 if ( childHorizontalStretch > 0 && childHorizontalStretch > existingColumnStretch )
3009 {
3010 gbLayout->setColumnStretch( widgetColumn, childHorizontalStretch );
3011 }
3012
3013 if ( childDef->verticalStretch() > 0 && childDef->verticalStretch() > gbLayout->rowStretch( row ) )
3014 {
3015 gbLayout->setRowStretch( row, childDef->verticalStretch() );
3016 }
3017
3018 if ( column >= columnCount * 2 )
3019 {
3020 column = 0;
3021 row += 1;
3022 }
3023
3024 if ( widgetInfo.widget
3025 && widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Fixed
3026 && widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Maximum
3027 && widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Preferred )
3028 addSpacer = false;
3029
3030 // we consider all relation editors should be expanding
3031 if ( qobject_cast<QgsAttributeFormRelationEditorWidget *>( widgetInfo.widget ) )
3032 addSpacer = false;
3033 }
3034
3035 if ( addSpacer )
3036 {
3037 QWidget *spacer = new QWidget();
3038 spacer->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Preferred );
3039 gbLayout->addWidget( spacer, ++row, 0 );
3040 gbLayout->setRowStretch( row, 1 );
3041 }
3042
3043 newWidgetInfo.labelText = QString();
3044 newWidgetInfo.labelOnTop = true;
3045 newWidgetInfo.showLabel = widgetDef->showLabel();
3046 newWidgetInfo.expandingNeeded |= !addSpacer;
3047 break;
3048 }
3049
3051 {
3052 const QgsAttributeEditorQmlElement *elementDef = static_cast<const QgsAttributeEditorQmlElement *>( widgetDef );
3053 QgsQmlWidgetWrapper *qmlWrapper = new QgsQmlWidgetWrapper( mLayer, nullptr, this );
3054 qmlWrapper->setQmlCode( elementDef->qmlCode() );
3055 context.setAttributeFormMode( mMode );
3056 qmlWrapper->setContext( context );
3057
3058 mWidgets.append( qmlWrapper );
3059
3060 newWidgetInfo.widget = qmlWrapper->widget();
3061 newWidgetInfo.labelText = elementDef->name();
3062 newWidgetInfo.labelOnTop = true;
3063 newWidgetInfo.showLabel = widgetDef->showLabel();
3064 break;
3065 }
3066
3068 {
3069 const QgsAttributeEditorHtmlElement *elementDef = static_cast<const QgsAttributeEditorHtmlElement *>( widgetDef );
3070
3071 QgsHtmlWidgetWrapper *htmlWrapper = new QgsHtmlWidgetWrapper( mLayer, nullptr, this );
3072 context.setAttributeFormMode( mMode );
3073 htmlWrapper->setHtmlCode( elementDef->htmlCode() );
3074 htmlWrapper->reinitWidget();
3075 mWidgets.append( htmlWrapper );
3076
3077 newWidgetInfo.widget = htmlWrapper->widget();
3078 newWidgetInfo.labelText = elementDef->name();
3079 newWidgetInfo.labelOnTop = true;
3080 newWidgetInfo.showLabel = widgetDef->showLabel();
3081 mNeedsGeometry |= htmlWrapper->needsGeometry();
3082 break;
3083 }
3084
3086 {
3087 const QgsAttributeEditorTextElement *elementDef = static_cast<const QgsAttributeEditorTextElement *>( widgetDef );
3088
3089 QgsTextWidgetWrapper *textWrapper = new QgsTextWidgetWrapper( mLayer, nullptr, this );
3090 context.setAttributeFormMode( mMode );
3091 textWrapper->setText( elementDef->text() );
3092 textWrapper->reinitWidget();
3093 mWidgets.append( textWrapper );
3094
3095 newWidgetInfo.widget = textWrapper->widget();
3096 newWidgetInfo.labelText = elementDef->name();
3097 newWidgetInfo.labelOnTop = false;
3098 newWidgetInfo.showLabel = widgetDef->showLabel();
3099 mNeedsGeometry |= textWrapper->needsGeometry();
3100 break;
3101 }
3102
3104 {
3105 const QgsAttributeEditorSpacerElement *elementDef = static_cast<const QgsAttributeEditorSpacerElement *>( widgetDef );
3106 QgsSpacerWidgetWrapper *spacerWrapper = new QgsSpacerWidgetWrapper( mLayer, nullptr, this );
3107 spacerWrapper->setDrawLine( elementDef->drawLine() );
3108 context.setAttributeFormMode( mMode );
3109 mWidgets.append( spacerWrapper );
3110
3111 newWidgetInfo.widget = spacerWrapper->widget();
3112 newWidgetInfo.labelOnTop = false;
3113 newWidgetInfo.showLabel = false;
3114 break;
3115 }
3116
3117 default:
3118 QgsDebugError( u"Unknown attribute editor widget type encountered..."_s );
3119 break;
3120 }
3121
3122 return newWidgetInfo;
3123}
3124
3125void QgsAttributeForm::createWrappers()
3126{
3127 QList<QWidget *> myWidgets = findChildren<QWidget *>();
3128 const QList<QgsField> fields = mLayer->fields().toList();
3129
3130 const auto constMyWidgets = myWidgets;
3131 for ( QWidget *myWidget : constMyWidgets )
3132 {
3133 // Check the widget's properties for a relation definition
3134 QVariant vRel = myWidget->property( "qgisRelation" );
3135 if ( vRel.isValid() )
3136 {
3137 QgsRelationManager *relMgr = QgsProject::instance()->relationManager();
3138 QgsRelation relation = relMgr->relation( vRel.toString() );
3139 if ( relation.isValid() )
3140 {
3141 QgsRelationWidgetWrapper *rww = new QgsRelationWidgetWrapper( mLayer, relation, myWidget, this );
3142 rww->setConfig( mLayer->editFormConfig().widgetConfig( relation.id() ) );
3143 rww->setContext( mContext );
3144 rww->widget(); // Will initialize the widget
3145 mWidgets.append( rww );
3146 }
3147 }
3148 else
3149 {
3150 const auto constFields = fields;
3151 for ( const QgsField &field : constFields )
3152 {
3153 if ( field.name() == myWidget->objectName() )
3154 {
3155 int idx = mLayer->fields().lookupField( field.name() );
3156
3157 QgsEditorWidgetWrapper *eww = QgsGui::editorWidgetRegistry()->create( mLayer, idx, myWidget, this, mContext );
3158 mWidgets.append( eww );
3159 }
3160 }
3161 }
3162 }
3163}
3164
3165void QgsAttributeForm::afterWidgetInit()
3166{
3167 bool isFirstEww = true;
3168
3169 const auto constMWidgets = mWidgets;
3170 for ( QgsWidgetWrapper *ww : constMWidgets )
3171 {
3172 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
3173
3174 if ( eww )
3175 {
3176 if ( isFirstEww )
3177 {
3178 setFocusProxy( eww->widget() );
3179 isFirstEww = false;
3180 }
3181
3182 connect( eww, &QgsEditorWidgetWrapper::valuesChanged, this, &QgsAttributeForm::onAttributeChanged, Qt::UniqueConnection );
3183 connect( eww, &QgsEditorWidgetWrapper::constraintStatusChanged, this, &QgsAttributeForm::onConstraintStatusChanged, Qt::UniqueConnection );
3184 }
3185 else
3186 {
3187 QgsRelationWidgetWrapper *relationWidgetWrapper = qobject_cast<QgsRelationWidgetWrapper *>( ww );
3188 if ( relationWidgetWrapper )
3189 {
3190 connect( relationWidgetWrapper, &QgsRelationWidgetWrapper::relatedFeaturesChanged, this, &QgsAttributeForm::onRelatedFeaturesChanged, static_cast<Qt::ConnectionType>( Qt::UniqueConnection | Qt::QueuedConnection ) );
3191 }
3192 }
3193 }
3194}
3195
3196
3197bool QgsAttributeForm::eventFilter( QObject *object, QEvent *e )
3198{
3199 Q_UNUSED( object )
3200
3201 if ( e->type() == QEvent::KeyPress )
3202 {
3203 QKeyEvent *keyEvent = dynamic_cast<QKeyEvent *>( e );
3204 if ( keyEvent && keyEvent->key() == Qt::Key_Escape )
3205 {
3206 // Re-emit to this form so it will be forwarded to parent
3207 event( e );
3208 return true;
3209 }
3210 }
3211
3212 return false;
3213}
3214
3215void QgsAttributeForm::scanForEqualAttributes( QgsFeatureIterator &fit, QSet<int> &mixedValueFields, QHash<int, QVariant> &fieldSharedValues ) const
3216{
3217 mixedValueFields.clear();
3218 fieldSharedValues.clear();
3219
3220 QgsFeature f;
3221 bool first = true;
3222 while ( fit.nextFeature( f ) )
3223 {
3224 for ( int i = 0; i < mLayer->fields().count(); ++i )
3225 {
3226 if ( mixedValueFields.contains( i ) )
3227 continue;
3228
3229 if ( first )
3230 {
3231 fieldSharedValues[i] = f.attribute( i );
3232 }
3233 else
3234 {
3235 if ( fieldSharedValues.value( i ) != f.attribute( i ) )
3236 {
3237 fieldSharedValues.remove( i );
3238 mixedValueFields.insert( i );
3239 }
3240 }
3241 }
3242 first = false;
3243
3244 if ( mixedValueFields.count() == mLayer->fields().count() )
3245 {
3246 // all attributes are mixed, no need to keep scanning
3247 break;
3248 }
3249 }
3250}
3251
3252
3253void QgsAttributeForm::layerSelectionChanged()
3254{
3255 switch ( mMode )
3256 {
3264 break;
3265
3267 resetMultiEdit( true );
3268 break;
3269 }
3270}
3271
3273{
3274 mIsSettingMultiEditFeatures = true;
3275 mMultiEditFeatureIds = fids;
3276
3277 if ( fids.isEmpty() )
3278 {
3279 // no selected features
3280 QMultiMap<int, QgsAttributeFormEditorWidget *>::const_iterator wIt = mFormEditorWidgets.constBegin();
3281 for ( ; wIt != mFormEditorWidgets.constEnd(); ++wIt )
3282 {
3283 wIt.value()->initialize( QVariant() );
3284 }
3285 mIsSettingMultiEditFeatures = false;
3286 return;
3287 }
3288
3289 QgsFeatureIterator fit = mLayer->getFeatures( QgsFeatureRequest().setFilterFids( fids ) );
3290
3291 // Scan through all features to determine which attributes are initially the same
3292 QSet<int> mixedValueFields;
3293 QHash<int, QVariant> fieldSharedValues;
3294 scanForEqualAttributes( fit, mixedValueFields, fieldSharedValues );
3295
3296 // also fetch just first feature
3297 fit = mLayer->getFeatures( QgsFeatureRequest().setFilterFid( *fids.constBegin() ) );
3298 QgsFeature firstFeature;
3299 fit.nextFeature( firstFeature );
3300
3301 // Make this feature the current form feature or the constraints will be evaluated
3302 // on a possibly wrong previously selected/current feature
3303 if ( mCurrentFormFeature.id() != firstFeature.id() )
3304 {
3305 setFeature( firstFeature );
3306 }
3307
3308 const auto constMixedValueFields = mixedValueFields;
3309 for ( int fieldIndex : std::as_const( mixedValueFields ) )
3310 {
3311 const QList<QgsAttributeFormEditorWidget *> formEditorWidgets = mFormEditorWidgets.values( fieldIndex );
3312 if ( formEditorWidgets.isEmpty() )
3313 continue;
3314
3315 const QStringList additionalFields = formEditorWidgets.first()->editorWidget()->additionalFields();
3316 QVariantList additionalFieldValues;
3317 for ( const QString &additionalField : additionalFields )
3318 additionalFieldValues << firstFeature.attribute( additionalField );
3319
3320 for ( QgsAttributeFormEditorWidget *w : formEditorWidgets )
3321 w->initialize( firstFeature.attribute( fieldIndex ), true, additionalFieldValues );
3322 }
3323 QHash<int, QVariant>::const_iterator sharedValueIt = fieldSharedValues.constBegin();
3324 for ( ; sharedValueIt != fieldSharedValues.constEnd(); ++sharedValueIt )
3325 {
3326 const QList<QgsAttributeFormEditorWidget *> formEditorWidgets = mFormEditorWidgets.values( sharedValueIt.key() );
3327 if ( formEditorWidgets.isEmpty() )
3328 continue;
3329
3330 bool mixed = false;
3331 const QStringList additionalFields = formEditorWidgets.first()->editorWidget()->additionalFields();
3332 for ( const QString &additionalField : additionalFields )
3333 {
3334 int index = mLayer->fields().indexFromName( additionalField );
3335 if ( constMixedValueFields.contains( index ) )
3336 {
3337 // if additional field are mixed, it is considered as mixed
3338 mixed = true;
3339 break;
3340 }
3341 }
3342 QVariantList additionalFieldValues;
3343 if ( mixed )
3344 {
3345 for ( const QString &additionalField : additionalFields )
3346 additionalFieldValues << firstFeature.attribute( additionalField );
3347 for ( QgsAttributeFormEditorWidget *w : formEditorWidgets )
3348 w->initialize( firstFeature.attribute( sharedValueIt.key() ), true, additionalFieldValues );
3349 }
3350 else
3351 {
3352 for ( const QString &additionalField : additionalFields )
3353 {
3354 int index = mLayer->fields().indexFromName( additionalField );
3355 Q_ASSERT( fieldSharedValues.contains( index ) );
3356 additionalFieldValues << fieldSharedValues.value( index );
3357 }
3358 for ( QgsAttributeFormEditorWidget *w : formEditorWidgets )
3359 w->initialize( sharedValueIt.value(), false, additionalFieldValues );
3360 }
3361 }
3362
3363 setMultiEditFeatureIdsRelations( fids );
3364
3365 mIsSettingMultiEditFeatures = false;
3366}
3367
3369{
3370 if ( mOwnsMessageBar )
3371 delete mMessageBar;
3372 mOwnsMessageBar = false;
3373 mMessageBar = messageBar;
3374}
3375
3377{
3379 {
3380 Q_ASSERT( false );
3381 }
3382
3383 QStringList filters;
3384 for ( QgsAttributeFormWidget *widget : mFormWidgets )
3385 {
3386 QString filter = widget->currentFilterExpression();
3387 if ( !filter.isNull() )
3388 filters << '(' + filter + ')';
3389 }
3390
3391 return filters.join( " AND "_L1 );
3392}
3393
3395{
3396 mExtraContextScope.reset( extraScope );
3397}
3398
3399void QgsAttributeForm::ContainerInformation::apply( QgsExpressionContext *expressionContext )
3400{
3401 const bool newVisibility = expression.evaluate( expressionContext ).toBool();
3402
3403 if ( expression.isValid() && !expression.hasEvalError() && newVisibility != isVisible )
3404 {
3405 if ( tabWidget )
3406 {
3407 tabWidget->setTabVisible( widget, newVisibility );
3408 }
3409 else
3410 {
3411 widget->setVisible( newVisibility );
3412 }
3413
3414 isVisible = newVisibility;
3415 }
3416
3417 const bool newCollapsedState = collapsedExpression.evaluate( expressionContext ).toBool();
3418
3419 if ( collapsedExpression.isValid() && !collapsedExpression.hasEvalError() && newCollapsedState != isCollapsed )
3420 {
3421 if ( QgsCollapsibleGroupBoxBasic * collapsibleGroupBox { qobject_cast<QgsCollapsibleGroupBoxBasic *>( widget ) } )
3422 {
3423 collapsibleGroupBox->setCollapsed( newCollapsedState );
3424 isCollapsed = newCollapsedState;
3425 }
3426 }
3427}
3428
3429void QgsAttributeForm::updateJoinedFields( const QgsEditorWidgetWrapper &eww )
3430{
3431 if ( !eww.layer()->fields().exists( eww.fieldIdx() ) )
3432 return;
3433
3434 QgsFeature formFeature;
3435 QgsField field = eww.layer()->fields().field( eww.fieldIdx() );
3436 QList<const QgsVectorLayerJoinInfo *> infos = eww.layer()->joinBuffer()->joinsWhereFieldIsId( field );
3437
3438 if ( infos.count() == 0 || !currentFormValuesFeature( formFeature ) )
3439 return;
3440
3441 const QString hint = tr( "No feature joined" );
3442 const auto constInfos = infos;
3443 for ( const QgsVectorLayerJoinInfo *info : constInfos )
3444 {
3445 if ( !info->isDynamicFormEnabled() )
3446 continue;
3447
3448 QgsFeature joinFeature = mLayer->joinBuffer()->joinedFeatureOf( info, formFeature );
3449
3450 mJoinedFeatures[info] = joinFeature;
3451
3452 if ( info->hasSubset() )
3453 {
3454 const QStringList subsetNames = QgsVectorLayerJoinInfo::joinFieldNamesSubset( *info );
3455
3456 const auto constSubsetNames = subsetNames;
3457 for ( const QString &field : constSubsetNames )
3458 {
3459 QString prefixedName = info->prefixedFieldName( field );
3460 QVariant val;
3461 QString hintText = hint;
3462
3463 if ( joinFeature.isValid() )
3464 {
3465 val = joinFeature.attribute( field );
3466 hintText.clear();
3467 }
3468
3469 changeAttribute( prefixedName, val, hintText );
3470 }
3471 }
3472 else
3473 {
3474 const QgsFields joinFields = joinFeature.fields();
3475 for ( const QgsField &field : joinFields )
3476 {
3477 QString prefixedName = info->prefixedFieldName( field );
3478 QVariant val;
3479 QString hintText = hint;
3480
3481 if ( joinFeature.isValid() )
3482 {
3483 val = joinFeature.attribute( field.name() );
3484 hintText.clear();
3485 }
3486
3487 changeAttribute( prefixedName, val, hintText );
3488 }
3489 }
3490 }
3491}
3492
3493bool QgsAttributeForm::fieldIsEditable( int fieldIndex ) const
3494{
3497}
3498
3499void QgsAttributeForm::updateFieldDependencies()
3500{
3501 mDefaultValueDependencies.clear();
3502 mVirtualFieldsDependencies.clear();
3503 mRelatedLayerFieldsDependencies.clear();
3504 mParentDependencies.clear();
3505
3506 //create defaultValueDependencies
3507 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
3508 {
3509 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
3510 if ( !eww )
3511 continue;
3512
3513 updateFieldDependenciesParent( eww );
3514 updateFieldDependenciesDefaultValue( eww );
3515 updateFieldDependenciesVirtualFields( eww );
3516 updateRelatedLayerFieldsDependencies( eww );
3517 }
3518}
3519
3520void QgsAttributeForm::updateFieldDependenciesDefaultValue( QgsEditorWidgetWrapper *eww )
3521{
3522 QgsExpression exp( eww->field().defaultValueDefinition().expression() );
3523
3524 if ( exp.needsGeometry() )
3525 mNeedsGeometry = true;
3526
3527 //if a function requires all attributes, it should have the dependency of every field change
3528 if ( exp.referencedColumns().contains( QgsFeatureRequest::ALL_ATTRIBUTES ) )
3529 {
3530 const QList<int> allAttributeIds( mLayer->fields().allAttributesList() );
3531
3532 for ( const int id : allAttributeIds )
3533 {
3534 mDefaultValueDependencies.insertMulti( id, eww );
3535 }
3536 }
3537 else
3538 {
3539 //otherwise just enter for the field depending on
3540 const QSet<QString> referencedColumns = exp.referencedColumns();
3541 for ( const QString &referencedColumn : referencedColumns )
3542 {
3543 mDefaultValueDependencies.insertMulti( mLayer->fields().lookupField( referencedColumn ), eww );
3544 }
3545 }
3546}
3547
3548void QgsAttributeForm::updateFieldDependenciesVirtualFields( QgsEditorWidgetWrapper *eww )
3549{
3550 QString expressionField = eww->layer()->expressionField( eww->fieldIdx() );
3551 if ( expressionField.isEmpty() )
3552 return;
3553
3554 QgsExpression exp( expressionField );
3555
3556 if ( exp.needsGeometry() )
3557 mNeedsGeometry = true;
3558
3559 //if a function requires all attributes, it should have the dependency of every field change
3560 if ( exp.referencedColumns().contains( QgsFeatureRequest::ALL_ATTRIBUTES ) )
3561 {
3562 const QList<int> allAttributeIds( mLayer->fields().allAttributesList() );
3563
3564 for ( const int id : allAttributeIds )
3565 {
3566 mVirtualFieldsDependencies.insertMulti( id, eww );
3567 }
3568 }
3569 else
3570 {
3571 //otherwise just enter for the field depending on
3572 const QSet<QString> referencedColumns = exp.referencedColumns();
3573 for ( const QString &referencedColumn : referencedColumns )
3574 {
3575 mVirtualFieldsDependencies.insertMulti( mLayer->fields().lookupField( referencedColumn ), eww );
3576 }
3577 }
3578}
3579
3580void QgsAttributeForm::updateRelatedLayerFieldsDependencies( QgsEditorWidgetWrapper *eww )
3581{
3582 if ( eww )
3583 {
3584 QString expressionField = eww->layer()->expressionField( eww->fieldIdx() );
3585 if ( expressionField.contains( u"relation_aggregate"_s ) || expressionField.contains( u"get_features"_s ) )
3586 mRelatedLayerFieldsDependencies.insert( eww );
3587 }
3588 else
3589 {
3590 mRelatedLayerFieldsDependencies.clear();
3591 //create defaultValueDependencies
3592 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
3593 {
3594 QgsEditorWidgetWrapper *editorWidgetWrapper = qobject_cast<QgsEditorWidgetWrapper *>( ww );
3595 if ( !editorWidgetWrapper )
3596 continue;
3597
3598 updateRelatedLayerFieldsDependencies( editorWidgetWrapper );
3599 }
3600 }
3601}
3602
3603void QgsAttributeForm::updateFieldDependenciesParent( QgsEditorWidgetWrapper *eww )
3604{
3605 if ( eww && !eww->field().defaultValueDefinition().expression().isEmpty() )
3606 {
3607 const QgsExpression expression( eww->field().defaultValueDefinition().expression() );
3608 const QSet<QString> referencedVariablesAndFunctions = expression.referencedVariables() + expression.referencedFunctions();
3609 for ( const QString &referenced : referencedVariablesAndFunctions )
3610 {
3611 if ( referenced.startsWith( "current_parent"_L1 ) )
3612 {
3613 mParentDependencies.insert( eww );
3614 break;
3615 }
3616 }
3617 }
3618}
3619
3620void QgsAttributeForm::setMultiEditFeatureIdsRelations( const QgsFeatureIds &fids )
3621{
3622 for ( QgsAttributeFormWidget *formWidget : mFormWidgets )
3623 {
3624 QgsAttributeFormRelationEditorWidget *relationEditorWidget = dynamic_cast<QgsAttributeFormRelationEditorWidget *>( formWidget );
3625 if ( !relationEditorWidget )
3626 continue;
3627
3628 relationEditorWidget->setMultiEditFeatureIds( fids );
3629 }
3630}
3631
3632void QgsAttributeForm::updateIcon( QgsEditorWidgetWrapper *eww )
3633{
3634 if ( !eww->widget() || !mIconMap[eww->widget()] )
3635 return;
3636
3637 // no icon by default
3638 mIconMap[eww->widget()]->hide();
3639
3640 if ( !eww->widget()->isEnabled() && mLayer->isEditable() )
3641 {
3642 if ( mLayer->fields().fieldOrigin( eww->fieldIdx() ) == Qgis::FieldOrigin::Join )
3643 {
3644 int srcFieldIndex;
3645 const QgsVectorLayerJoinInfo *info = mLayer->joinBuffer()->joinForFieldIndex( eww->fieldIdx(), mLayer->fields(), srcFieldIndex );
3646
3647 if ( !info )
3648 return;
3649
3650 if ( !info->isEditable() )
3651 {
3652 const QString file = u"/mIconJoinNotEditable.svg"_s;
3653 const QString tooltip = tr( "Join settings do not allow editing" );
3654 reloadIcon( file, tooltip, mIconMap[eww->widget()] );
3655 }
3656 else if ( mMode == QgsAttributeEditorContext::AddFeatureMode && !info->hasUpsertOnEdit() )
3657 {
3658 const QString file = u"mIconJoinHasNotUpsertOnEdit.svg"_s;
3659 const QString tooltip = tr( "Join settings do not allow upsert on edit" );
3660 reloadIcon( file, tooltip, mIconMap[eww->widget()] );
3661 }
3662 else if ( !info->joinLayer()->isEditable() )
3663 {
3664 const QString file = u"/mIconJoinedLayerNotEditable.svg"_s;
3665 const QString tooltip = tr( "Joined layer is not toggled editable" );
3666 reloadIcon( file, tooltip, mIconMap[eww->widget()] );
3667 }
3668 }
3669 }
3670}
3671
3672void QgsAttributeForm::reloadIcon( const QString &file, const QString &tooltip, QSvgWidget *sw )
3673{
3674 sw->load( QgsApplication::iconPath( file ) );
3675 sw->setToolTip( tooltip );
3676 sw->show();
3677}
3678
3680{
3681 const bool reuseAllLastValues = QgsSettingsRegistryCore::settingsDigitizingReuseLastValues->value();
3682 QgsDebugMsgLevel( u"reuseAllLastValues: %1"_s.arg( reuseAllLastValues ), 2 );
3683
3684 const QgsFields fields = layer->fields();
3685 QgsAttributeMap initialAttributeValues;
3686 for ( int idx = 0; idx < fields.count(); ++idx )
3687 {
3688 if ( attributes.contains( idx ) )
3689 {
3690 initialAttributeValues.insert( idx, attributes.value( idx ) );
3691 }
3692 else if ( ( reuseAllLastValues || layer->editFormConfig().reuseLastValuePolicy( idx ) != Qgis::AttributeFormReuseLastValuePolicy::NotAllowed ) )
3693 {
3694 const QVariant lastUsedValuesVariant = layer->property( "AttributeFormLastUsedValues" );
3695 const QgsAttributeMap lastUsedValues = lastUsedValuesVariant.isValid() ? lastUsedValuesVariant.value<QgsAttributeMap>() : QgsAttributeMap();
3696 if ( lastUsedValues.contains( idx ) && layer->dataProvider() && layer->dataProvider()->defaultValueClause( idx ) != lastUsedValues[idx] )
3697 {
3698 initialAttributeValues.insert( idx, lastUsedValues[idx] );
3699 }
3700 }
3701 }
3702
3703 return QgsVectorLayerUtils::createFeature( layer, geometry, initialAttributeValues, &context );
3704}
@ Trusted
The project has been determined by the user as trusted.
Definition qgis.h:478
@ Row
A row of editors (horizontal layout).
Definition qgis.h:6041
AttributeFormReuseLastValuePolicy
Attribute form policy for reusing last entered values.
Definition qgis.h:6097
@ AllowedDefaultOn
Reuse of last values allowed and enabled by default.
Definition qgis.h:6099
@ NotAllowed
Reuse of last values not allowed.
Definition qgis.h:6098
@ File
Load the Python code from an external file.
Definition qgis.h:6085
@ Environment
Use the Python code available in the Python environment.
Definition qgis.h:6087
@ NoSource
Do not use Python code at all.
Definition qgis.h:6084
@ Dialog
Use the Python code provided in the dialog.
Definition qgis.h:6086
@ DragAndDrop
"Drag and drop" layout. Needs to be configured.
Definition qgis.h:6055
@ UiFile
Load a .ui file for the layout. Needs to be configured.
Definition qgis.h:6056
@ Warning
Warning message.
Definition qgis.h:162
@ Info
Information message.
Definition qgis.h:161
@ Success
Used for reporting a successful operation.
Definition qgis.h:164
@ Normal
A normal relation.
Definition qgis.h:4735
@ Join
Field originates from a joined layer.
Definition qgis.h:1827
@ Action
A layer action element.
Definition qgis.h:6025
@ Container
A container.
Definition qgis.h:6020
@ QmlElement
A QML element.
Definition qgis.h:6023
@ Relation
A relation.
Definition qgis.h:6022
@ HtmlElement
A HTML element.
Definition qgis.h:6024
@ TextElement
A text element.
Definition qgis.h:6026
@ SpacerElement
A spacer element.
Definition qgis.h:6027
SelectBehavior
Specifies how a selection should be applied.
Definition qgis.h:1891
@ SetSelection
Set selection, removing any existing selection.
Definition qgis.h:1892
@ AddToSelection
Add selection to current selection.
Definition qgis.h:1893
@ IntersectSelection
Modify current selection to include only select features which match.
Definition qgis.h:1894
@ RemoveFromSelection
Remove from current selection.
Definition qgis.h:1895
void setAction(const QgsAction &action)
Sets the action.
static QgsNetworkContentFetcherRegistry * networkContentFetcherRegistry()
Returns the application's network content registry used for fetching temporary files during QGIS sess...
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
static QString iconPath(const QString &iconFile)
Returns path to the desired icon file.
const QgsAction & action(const QgsVectorLayer *layer) const
Returns the (possibly lazy loaded) action for the given layer.
QgsOptionalExpression visibilityExpression() const
The visibility expression is used in the attribute form to show or hide this container based on an ex...
QgsOptionalExpression collapsedExpression() const
The collapsed expression is used in the attribute form to set the collapsed status of the group box c...
bool collapsed() const
For group box containers returns true if this group box is collapsed.
Qgis::AttributeEditorContainerType type() const
Returns the container type.
QList< QgsAttributeEditorElement * > children() const
Gets a list of the children elements of this container.
QColor backgroundColor() const
Returns the background color of the container.
int columnCount() const
Gets the number of columns in this group.
Contains context information for attribute editor widgets.
FormMode formMode() const
Returns the form mode.
@ Embed
A form was embedded as a widget on another form.
@ SearchMode
Form values are used for searching/filtering the layer.
@ FixAttributeMode
Fix feature mode, for modifying the feature attributes without saving. The updated feature is availab...
@ IdentifyMode
Identify the feature.
@ SingleEditMode
Single edit mode, for editing a single feature.
@ AggregateSearchMode
Form is in aggregate search mode, show each widget in this mode.
@ MultiEditMode
Multi edit mode, for editing fields of multiple features at once.
@ PreviewMode
Preview mode, for previewing attribute configurations.
void setAttributeFormMode(const Mode &attributeFormMode)
Set attributeFormMode for the edited form.
An abstract base class for any elements of a drag and drop form.
LabelStyle labelStyle() const
Returns the label style.
Qgis::AttributeEditorType type() const
The type of this element.
bool showLabel() const
Controls if this element should be labeled with a title (field, relation or groupname).
QString name() const
Returns the name of this element.
int idx() const
Returns the index of the field.
QString htmlCode() const
The Html code that will be represented within this widget.
QString qmlCode() const
The QML code that will be represented within this widget.
const QgsRelation & relation() const
Gets the id of the relation which shall be embedded.
QVariantMap relationEditorConfiguration() const
Returns the relation editor widget configuration.
QVariant nmRelationId() const
Determines the relation id of the second relation involved in an N:M relation.
bool forceSuppressFormPopup() const
Determines the force suppress form popup status.
QString relationWidgetTypeId() const
Returns the current relation widget type id.
QString label() const
Determines the label of this element.
bool drawLine() const
Returns true if the spacer element will contain an horizontal line.
QString text() const
The Text that will be represented within this widget.
A widget consisting of both an editor widget and additional widgets for controlling the behavior of t...
void setRememberLastValue(bool remember)
Sets whether the widget value will be remembered for potential reuse when creating new features.
void rememberLastValueChanged(int index, bool remember)
Emitted when the widget's remember last value toggle changes.
QgsEditorWidgetWrapper * editorWidget() const
Returns the editor widget wrapper.
void createSearchWidgetWrappers(const QgsAttributeEditorContext &context=QgsAttributeEditorContext()) override
Creates the search widget wrappers for the widget used when the form is in search mode.
Interface class for custom attribute forms.
Widget to show for child relations on an attribute form.
void setMultiEditFeatureIds(const QgsFeatureIds &fids)
Set multiple feature to edit simultaneously.
void createSearchWidgetWrappers(const QgsAttributeEditorContext &context=QgsAttributeEditorContext()) override
Creates the search widget wrappers for the widget used when the form is in search mode.
Base class for all widgets shown on a QgsAttributeForm.
@ SearchMode
Layer search/filter mode.
@ MultiEditMode
Multi edit mode, both the editor widget and a QgsMultiEditToolButton is shown.
@ DefaultMode
Default mode, only the editor widget is shown.
@ AggregateSearchMode
Embedded in a search form, show additional aggregate function toolbutton.
void showButtonBox()
Shows the button box (OK/Cancel) and disables auto-commit.
void parentFormValueChanged(const QString &attribute, const QVariant &newValue)
Is called in embedded forms when an attribute value in the parent form has changed to newValue.
QgsVectorLayer * layer()
Returns the layer for which this form is shown.
void setFeature(const QgsFeature &feature)
Update all editors to correspond to a different feature.
void setMultiEditFeatureIds(const QgsFeatureIds &fids)
Sets all feature IDs which are to be edited if the form is in multiedit mode.
void refreshFeature()
reload current feature
void beforeSave(bool &ok)
Will be emitted before the feature is saved.
void changeGeometry(const QgsGeometry &geometry)
Changes the geometry of the feature attached to the form.
bool eventFilter(QObject *object, QEvent *event) override
Intercepts keypress on custom form (escape should not close it).
bool saveWithDetails(QString *error=nullptr)
Save all the values from the editors to the layer.
void addInterface(QgsAttributeFormInterface *iface)
Takes ownership.
bool needsGeometry() const
Returns true if any of the form widgets need feature geometry.
void setExtraContextScope(QgsExpressionContextScope *extraScope)
Sets an additional expression context scope to be used for calculations in this form.
void changeAttribute(const QString &field, const QVariant &value, const QString &hintText=QString())
Call this to change the content of a given attribute.
bool save()
Save all the values from the editors to the layer.
void filterExpressionSet(const QString &expression, QgsAttributeForm::FilterType type)
Emitted when a filter expression is set using the form.
void hideButtonBox()
Hides the button box (OK/Cancel) and enables auto-commit.
void closed()
Emitted when the user selects the close option from the form's button bar.
void resetSearch()
Resets the search/filter form values.
void widgetValueChanged(const QString &attribute, const QVariant &value, bool attributeChanged)
Notifies about changes of attributes.
void openFilteredFeaturesAttributeTable(const QString &filter)
Emitted when the user chooses to open the attribute table dialog with a filtered set of features.
QString aggregateFilter() const
The aggregate filter is only useful if the form is in AggregateFilter mode.
void flashFeatures(const QString &filter)
Emitted when the user chooses to flash a filtered set of features.
void resetValues()
Sets all values to the values of the current feature.
QgsAttributeForm(QgsVectorLayer *vl, const QgsFeature &feature=QgsFeature(), const QgsAttributeEditorContext &context=QgsAttributeEditorContext(), QWidget *parent=nullptr)
void disconnectButtonBox()
Disconnects the button box (OK/Cancel) from the accept/resetValues slots If this method is called,...
bool editable()
Returns if the form is currently in editable mode.
void setMessageBar(QgsMessageBar *messageBar)
Sets the message bar to display feedback from the form in.
void modeChanged(QgsAttributeEditorContext::Mode mode)
Emitted when the form changes mode.
static QgsFeature createFeature(QgsVectorLayer *layer, const QgsGeometry &geometry, const QgsAttributeMap &attributes, QgsExpressionContext &context)
Creates a new feature for a given layer taking into account attribute form-specific context such as t...
const QgsFeature & feature() const
Returns feature of attribute form.
void featureSaved(const QgsFeature &feature)
Emitted when a feature is changed or added.
void displayWarning(const QString &message)
Displays a warning message in the form message bar.
void zoomToFeatures(const QString &filter)
Emitted when the user chooses to zoom to a filtered set of features.
Q_DECL_DEPRECATED void attributeChanged(const QString &attribute, const QVariant &value)
Notifies about changes of attributes, this signal is not emitted when the value is set back to the or...
void setMode(QgsAttributeEditorContext::Mode mode)
Sets the current mode of the form.
@ ReplaceFilter
Filter should replace any existing filter.
@ FilterOr
Filter should be combined using "OR".
@ FilterAnd
Filter should be combined using "AND".
QgsAttributeEditorContext::Mode mode() const
Returns the current mode of the form.
void setStyleSheet(const QString &style)
Overridden to prepare base call and avoid crash due to specific QT versions.
void setCollapsed(bool collapse)
Collapse or uncollapse this groupbox.
QgsEditorWidgetSetup findBest(const QgsVectorLayer *vl, const QString &fieldName) const
Find the best editor widget and its configuration for a given field.
QgsEditorWidgetWrapper * create(const QString &widgetId, QgsVectorLayer *vl, int fieldIdx, const QVariantMap &config, QWidget *editor, QWidget *parent, const QgsAttributeEditorContext &context=QgsAttributeEditorContext())
Create an attribute editor widget wrapper of a given type for a given field.
QString type() const
Returns the widget type to use.
QVariantMap config() const
Returns the widget configuration.
Manages an editor widget.
virtual QVariant value() const =0
Will be used to access the widget's value.
virtual QVariantList additionalFieldValues() const
Will be used to access the widget's values for potential additional fields handled by the widget.
int fieldIdx() const
Access the field index.
virtual void parentFormValueChanged(const QString &attribute, const QVariant &value)
Is called in embedded form widgets when an attribute value in the parent form has changed.
virtual QStringList additionalFields() const
Returns the list of additional fields which the editor handles.
QString constraintFailureReason() const
Returns the reason why a constraint check has failed (or an empty string if constraint check was succ...
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 isValidConstraint() const
Gets the current constraint status.
void updateConstraint(const QgsFeature &featureContext, QgsFieldConstraints::ConstraintOrigin constraintOrigin=QgsFieldConstraints::ConstraintOriginNotSet)
Update constraint.
void setValues(const QVariant &value, const QVariantList &additionalValues)
Is called when the value of the widget or additional field values needs to be changed.
virtual void setHint(const QString &hintText)
Add a hint text on the widget.
QgsField field() const
Access the field.
ConstraintResult
Result of constraint checks.
void constraintStatusChanged(const QString &constraint, const QString &desc, const QString &err, QgsEditorWidgetWrapper::ConstraintResult status)
Emit this signal when the constraint status changed.
virtual void setValue(const QVariant &value)
Is called when the value of the widget needs to be changed.
bool isBlockingCommit() const
Returns true if the widget is preventing the feature from being committed.
void setConstraintResultVisible(bool constraintResultVisible)
Sets whether the constraint result is visible.
Single scope for storing variables and functions for use within a QgsExpressionContext.
static QgsExpressionContextScope * parentFormScope(const QgsFeature &formFeature=QgsFeature(), const QString &formMode=QString())
Creates a new scope which contains functions and variables from the current parent attribute form/tab...
static QgsExpressionContextScope * formScope(const QgsFeature &formFeature=QgsFeature(), const QString &formMode=QString())
Creates a new scope which contains functions and variables from the current attribute form/table form...
static QList< QgsExpressionContextScope * > globalProjectLayerScopes(const QgsMapLayer *layer)
Creates a list of three scopes: global, layer's project and layer.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
void appendScope(QgsExpressionContextScope *scope)
Appends a scope to the end of the context.
void setFeature(const QgsFeature &feature)
Convenience function for setting a feature for the context.
void appendScopes(const QList< QgsExpressionContextScope * > &scopes)
Appends a list of scopes to the end of the context.
QSet< QString > referencedColumns() const
Gets list of columns referenced by the expression.
bool hasEvalError() const
Returns true if an error occurred when evaluating last input.
QVariant evaluate()
Evaluate the feature and return the result.
bool isValid() const
Checks if this expression is valid.
Wrapper for iterator of features from vector data provider or vector layer.
bool nextFeature(QgsFeature &f)
Fetch next feature and stores in f, returns true on success.
Wraps a request for features to a vector layer (or directly its vector data provider).
static const QString ALL_ATTRIBUTES
A special attribute that if set matches all attributes.
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition qgsfeature.h:60
Q_INVOKABLE bool setAttribute(int field, const QVariant &attr)
Sets an attribute's value by field index.
QgsAttributes attributes
Definition qgsfeature.h:64
QgsFields fields
Definition qgsfeature.h:65
QgsFeatureId id
Definition qgsfeature.h:63
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
bool isValid() const
Returns the validity of this feature.
Q_INVOKABLE QVariant attribute(const QString &name) const
Lookup attribute value by attribute name.
ConstraintOrigin
Origin of constraints.
@ ConstraintOriginNotSet
Constraint is not set.
@ ConstraintOriginLayer
Constraint was set by layer.
QString constraintExpression() const
Returns the constraint expression for the field, if set.
static QString fieldToolTipExtended(const QgsField &field, const QgsVectorLayer *layer, const QString &predefinedComment=QString())
Returns a HTML formatted tooltip string for a field, containing details like the field name,...
QString name
Definition qgsfield.h:65
QString displayName() const
Returns the name to use when displaying this field.
Definition qgsfield.cpp:96
QgsDefaultValue defaultValueDefinition
Definition qgsfield.h:67
QString customComment
Definition qgsfield.h:71
QString comment
Definition qgsfield.h:64
QgsFieldConstraints constraints
Definition qgsfield.h:68
Container of fields for a vector layer.
Definition qgsfields.h:46
int count
Definition qgsfields.h:50
QList< QgsField > toList() const
Utility function to return a list of QgsField instances.
QgsField field(int fieldIdx) const
Returns the field at particular index (must be in range 0..N-1).
Qgis::FieldOrigin fieldOrigin(int fieldIdx) const
Returns the field's origin (value from an enumeration).
Q_INVOKABLE bool exists(int i) const
Returns if a field index is valid.
int size() const
Returns number of items.
QgsField at(int i) const
Returns the field at particular index (must be in range 0..N-1).
Q_INVOKABLE int lookupField(const QString &fieldName) const
Looks up field's index from the field name.
A geometry is the spatial representation of a feature.
static QgsEditorWidgetRegistry * editorWidgetRegistry()
Returns the global editor widget registry, used for managing all known edit widget factories.
Definition qgsgui.cpp:109
static bool allowExecutionOfEmbeddedScripts(QgsProject *project, QgsMessageBar *messageBar=nullptr)
Returns true if python embedded in a project is currently allowed to be loaded.
Definition qgsgui.cpp:398
void reinitWidget()
Clears the content and makes new initialization.
void setHtmlCode(const QString &htmlCode)
Sets the HTML code to htmlCode.
bool needsGeometry() const
Returns true if the widget needs feature geometry.
static void warning(const QString &msg)
Goes to qWarning.
void editingStopped()
Emitted when edited changes have been successfully written to the data provider.
void editingStarted()
Emitted when editing on this layer has started.
A bar for displaying non-blocking messages to the user.
bool popWidget(QgsMessageBarItem *item)
Remove the specified item from the bar, and display the next most recent one in the stack.
QFile * localFile(const QString &filePathOrUrl)
Returns a QFile from a local file or to a temporary file previously fetched by the registry.
bool enabled() const
Check if this optional is enabled.
Definition qgsoptional.h:76
T data() const
Access the payload data.
Definition qgsoptional.h:94
static Qgis::ProjectTrustStatus checkUserTrust(QgsProject *project)
Returns the current trust status of the specified project.
QgsRelationManager * relationManager
Definition qgsproject.h:125
static QgsProject * instance()
Returns the QgsProject singleton instance.
static bool run(const QString &command, const QString &messageOnError=QString())
Execute a Python statement.
static bool eval(const QString &command, QString &result)
Eval a Python statement.
void setQmlCode(const QString &qmlCode)
writes the qmlCode into a temporary file
QList< QgsRelation > referencedRelations(const QgsVectorLayer *layer=nullptr) const
Gets all relations where this layer is the referenced part (i.e.
QList< QgsRelation > referencingRelations(const QgsVectorLayer *layer=nullptr, int fieldIdx=-2) const
Gets all relations where the specified layer (and field) is the referencing part (i....
Q_INVOKABLE QgsRelation relation(const QString &id) const
Gets access to a relation by its id.
A widget wrapper for relation widgets.
void relatedFeaturesChanged()
Emit this signal, whenever the related features changed.
QgsRelation relation() const
The relation for which this wrapper is created.
void setWidgetConfig(const QVariantMap &config)
Will set the config of this widget wrapper to the specified config.
void setForceSuppressFormPopup(bool forceSuppressFormPopup)
Sets force suppress form popup status to forceSuppressFormPopup for this widget and for the vectorLay...
void setNmRelationId(const QVariant &nmRelationId=QVariant())
Sets nmRelationId for the relation id of the second relation involved in an N:M relation.
Represents a relationship between two vector layers.
Definition qgsrelation.h:42
QString name
Definition qgsrelation.h:52
Qgis::RelationshipType type() const
Returns the type of the relation.
QString id
Definition qgsrelation.h:45
QList< QgsRelation::FieldPair > fieldPairs() const
Returns the field pairs which form this relation The first element of each pair are the field names o...
static const QgsSettingsEntryBool * settingsDigitizingReuseLastValues
Settings entry digitizing reuseLastValues.
void setDrawLine(bool drawLine)
Sets a flag to define if the spacer element will contain an horizontal line.
void setTabVisible(QWidget *tab, bool visible)
Control the visibility for the tab with the given widget.
void setTabStyle(int tabIndex, const QgsAttributeEditorElement::LabelStyle &labelStyle)
Sets the optional custom labelStyle for the tab identified by tabIndex.
bool isInvalidJSON() const
Returns whether the text edit widget contains Invalid JSON.
bool needsGeometry() const
Returns true if the widget needs feature geometry.
void setText(const QString &text)
Sets the text code to htmlCode.
void reinitWidget()
Clears the content and makes new initialization.
static bool isNull(const QVariant &variant, bool silenceNullWarnings=false)
Returns true if the specified variant should be considered a NULL value.
static QVariant createNullVariant(QMetaType::Type metaType)
Helper method to properly create a null QVariant from a metaType Returns the created QVariant.
const QgsVectorLayerJoinInfo * joinForFieldIndex(int index, const QgsFields &fields, int &sourceFieldIndex) const
Finds the vector join for a layer field index.
QList< const QgsVectorLayerJoinInfo * > joinsWhereFieldIsId(const QgsField &field) const
Returns joins where the field of a target layer is considered as an id.
QStringList * joinFieldNamesSubset() const
Returns the subset of fields to be used from joined layer.
bool isDynamicFormEnabled() const
Returns whether the form has to be dynamically updated with joined fields when a feature is being cre...
bool hasUpsertOnEdit() const
Returns whether a feature created on the target layer has to impact the joined layer by creating a ne...
bool isEditable() const
Returns whether joined fields may be edited through the form of the target layer.
QgsVectorLayer * joinLayer() const
Returns joined layer (may be nullptr if the reference was set by layer ID and not resolved yet).
static bool fieldIsEditable(const QgsVectorLayer *layer, int fieldIndex, const QgsFeature &feature, QgsVectorLayerUtils::FieldIsEditableFlags flags=QgsVectorLayerUtils::FieldIsEditableFlags())
Tests whether a field is editable for a particular feature.
static QgsFeature createFeature(const QgsVectorLayer *layer, const QgsGeometry &geometry=QgsGeometry(), const QgsAttributeMap &attributes=QgsAttributeMap(), QgsExpressionContext *context=nullptr)
Creates a new feature ready for insertion into a layer.
QFlags< FieldIsEditableFlag > FieldIsEditableFlags
@ IgnoreLayerEditability
Ignores the vector layer's editable state.
Represents a vector layer which manages a vector based dataset.
bool isEditable() const final
Returns true if the provider is in editing mode.
void beforeRemovingExpressionField(int idx)
Will be emitted, when an expression field is going to be deleted from this vector layer.
QString expressionField(int index) const
Returns the expression used for a given expression field.
Q_INVOKABLE void selectByExpression(const QString &expression, Qgis::SelectBehavior behavior=Qgis::SelectBehavior::SetSelection, QgsExpressionContext *context=nullptr)
Selects matching features using an expression.
QgsVectorLayerJoinBuffer * joinBuffer()
Returns the join buffer object.
void selectionChanged(const QgsFeatureIds &selected, const QgsFeatureIds &deselected, bool clearAndSelect)
Emitted when selection was changed.
void beforeAddingExpressionField(const QString &fieldName)
Will be emitted, when an expression field is going to be added to this vector layer.
void updatedFields()
Emitted whenever the fields available from this layer have been changed.
void beforeModifiedCheck() const
Emitted when the layer is checked for modifications. Use for last-minute additions.
Manages an editor widget.
QWidget * widget()
Access the widget managed by this wrapper.
void setConfig(const QVariantMap &config)
Will set the config of this wrapper to the specified config.
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.
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
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:657
#define Q_NOWARN_DEPRECATED_POP
Definition qgis.h:7766
#define Q_NOWARN_DEPRECATED_PUSH
Definition qgis.h:7765
QMap< int, QVariant > QgsAttributeMap
QSet< QgsFeatureId > QgsFeatureIds
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:63
#define QgsDebugError(str)
Definition qgslogger.h:59