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