QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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
30#include "qgsfeatureiterator.h"
31#include "qgsgui.h"
32#include "qgsproject.h"
33#include "qgspythonrunner.h"
38#include "qgsmessagebar.h"
39#include "qgsmessagebaritem.h"
42#include "qgsrelationmanager.h"
43#include "qgslogger.h"
44#include "qgstabwidget.h"
45#include "qgssettings.h"
46#include "qgsscrollarea.h"
48#include "qgsvectorlayerutils.h"
50#include "qgsqmlwidgetwrapper.h"
53#include "qgsapplication.h"
55#include "qgsfeaturerequest.h"
56#include "qgstexteditwrapper.h"
57#include "qgsfieldmodel.h"
59
60#include <QDir>
61#include <QTextStream>
62#include <QFileInfo>
63#include <QFile>
64#include <QFormLayout>
65#include <QGridLayout>
66#include <QKeyEvent>
67#include <QLabel>
68#include <QPushButton>
69#include <QUiLoader>
70#include <QMessageBox>
71#include <QToolButton>
72#include <QMenu>
73#include <QSvgWidget>
74
75int QgsAttributeForm::sFormCounter = 0;
76
77QgsAttributeForm::QgsAttributeForm( QgsVectorLayer *vl, const QgsFeature &feature, const QgsAttributeEditorContext &context, QWidget *parent )
78 : QWidget( parent )
79 , mLayer( vl )
80 , mOwnsMessageBar( true )
81 , mContext( context )
82 , mFormNr( sFormCounter++ )
83 , mIsSaving( false )
84 , mPreventFeatureRefresh( false )
85 , mIsSettingMultiEditFeatures( false )
86 , mUnsavedMultiEditChanges( false )
87 , mEditCommandMessage( tr( "Attributes changed" ) )
88 , mMode( QgsAttributeEditorContext::SingleEditMode )
89{
90 init();
91 initPython();
93
94 connect( vl, &QgsVectorLayer::updatedFields, this, &QgsAttributeForm::onUpdatedFields );
95 connect( vl, &QgsVectorLayer::beforeAddingExpressionField, this, &QgsAttributeForm::preventFeatureRefresh );
96 connect( vl, &QgsVectorLayer::beforeRemovingExpressionField, this, &QgsAttributeForm::preventFeatureRefresh );
97 connect( vl, &QgsVectorLayer::selectionChanged, this, &QgsAttributeForm::layerSelectionChanged );
98 connect( this, &QgsAttributeForm::modeChanged, this, &QgsAttributeForm::updateContainersVisibility );
99
100 updateContainersVisibility();
101 updateLabels();
102 updateEditableState();
103
104}
105
107{
108 cleanPython();
109 qDeleteAll( mInterfaces );
110}
111
113{
114 mButtonBox->hide();
115
116 // Make sure that changes are taken into account if somebody tries to figure out if there have been some
119}
120
122{
123 mButtonBox->show();
124
125 disconnect( mLayer, &QgsVectorLayer::beforeModifiedCheck, this, &QgsAttributeForm::save );
126}
127
129{
130 disconnect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsAttributeForm::save );
131 disconnect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsAttributeForm::resetValues );
132}
133
135{
136 mInterfaces.append( iface );
137}
138
140{
141 return mFeature.isValid() && mLayer->isEditable();
142}
143
145{
146 if ( mode == mMode )
147 return;
148
150 {
151 //switching out of multi edit mode triggers a save
152 if ( mUnsavedMultiEditChanges )
153 {
154 // prompt for save
155 int res = QMessageBox::question( this, tr( "Multiedit Attributes" ),
156 tr( "Apply changes to edited features?" ), QMessageBox::Yes | QMessageBox::No );
157 if ( res == QMessageBox::Yes )
158 {
159 save();
160 }
161 }
162 clearMultiEditMessages();
163 }
164 mUnsavedMultiEditChanges = false;
165
166 mMode = mode;
167
168 if ( mButtonBox->isVisible() && mMode == QgsAttributeEditorContext::SingleEditMode )
169 {
171 }
172 else
173 {
174 disconnect( mLayer, &QgsVectorLayer::beforeModifiedCheck, this, &QgsAttributeForm::save );
175 }
176
177 //update all form editor widget modes to match
178 for ( QgsAttributeFormWidget *w : std::as_const( mFormWidgets ) )
179 {
180 switch ( mode )
181 {
184 break;
185
188 break;
189
192 break;
193
196 break;
197
200 break;
201
204 break;
205
208 break;
209 }
210 }
211 //update all form editor widget modes to match
212 for ( QgsWidgetWrapper *w : std::as_const( mWidgets ) )
213 {
214 QgsAttributeEditorContext newContext = w->context();
215 newContext.setAttributeFormMode( mMode );
216 w->setContext( newContext );
217 }
218
219 bool relationWidgetsVisible = ( mMode != QgsAttributeEditorContext::AggregateSearchMode );
220 for ( QgsAttributeFormRelationEditorWidget *w : findChildren< QgsAttributeFormRelationEditorWidget * >() )
221 {
222 w->setVisible( relationWidgetsVisible );
223 }
224
225 switch ( mode )
226 {
228 setFeature( mFeature );
229 mSearchButtonBox->setVisible( false );
230 break;
231
233 synchronizeState();
234 mSearchButtonBox->setVisible( false );
235 break;
236
238 synchronizeState();
239 mSearchButtonBox->setVisible( false );
240 break;
241
243 resetMultiEdit( false );
244 synchronizeState();
245 mSearchButtonBox->setVisible( false );
246 break;
247
249 mSearchButtonBox->setVisible( true );
250 synchronizeState();
252 break;
253
255 mSearchButtonBox->setVisible( false );
256 synchronizeState();
258 break;
259
261 setFeature( mFeature );
262 synchronizeState();
263 mSearchButtonBox->setVisible( false );
264 break;
265 }
266
267 emit modeChanged( mMode );
268}
269
270void QgsAttributeForm::changeAttribute( const QString &field, const QVariant &value, const QString &hintText )
271{
272 const auto constMWidgets = mWidgets;
273 for ( QgsWidgetWrapper *ww : constMWidgets )
274 {
275 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
276 if ( eww )
277 {
278 if ( eww->field().name() == field )
279 {
280 eww->setValues( value, QVariantList() );
281 eww->setHint( hintText );
282 }
283 // see if the field is present in additional fields of the editor widget
284 int index = eww->additionalFields().indexOf( field );
285 if ( index >= 0 )
286 {
287 QVariant mainValue = eww->value();
288 QVariantList additionalFieldValues = eww->additionalFieldValues();
289 additionalFieldValues[index] = value;
290 eww->setValues( mainValue, additionalFieldValues );
291 eww->setHint( hintText );
292 }
293 }
294 }
295}
296
298{
299 mFeature.setGeometry( geometry );
300}
301
303{
304 mIsSettingFeature = true;
305 mFeature = feature;
306 mCurrentFormFeature = feature;
307
308 switch ( mMode )
309 {
314 {
315 resetValues();
316
317 synchronizeState();
318
319 // Settings of feature is done when we trigger the attribute form interface
320 // Issue https://github.com/qgis/QGIS/issues/29667
321 mIsSettingFeature = false;
322 const auto constMInterfaces = mInterfaces;
323 for ( QgsAttributeFormInterface *iface : constMInterfaces )
324 {
325 iface->featureChanged();
326 }
327 break;
328 }
331 {
332 resetValues();
333 break;
334 }
336 {
337 //ignore setFeature
338 break;
339 }
340 }
341 mIsSettingFeature = false;
342}
343
344bool QgsAttributeForm::saveEdits( QString *error )
345{
346 bool success = true;
347 bool changedLayer = false;
348
349 QgsFeature updatedFeature = QgsFeature( mFeature );
350 if ( mFeature.isValid() || mMode == QgsAttributeEditorContext::AddFeatureMode )
351 {
352 bool doUpdate = false;
353
354 // An add dialog should perform an action by default
355 // and not only if attributes have "changed"
357 doUpdate = true;
358
359 QgsAttributes src = mFeature.attributes();
360 QgsAttributes dst = mFeature.attributes();
361
362 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
363 {
364 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
365 if ( eww )
366 {
367 // check for invalid JSON values
368 QgsTextEditWrapper *textEdit = qobject_cast<QgsTextEditWrapper *>( eww );
369 if ( textEdit && textEdit->isInvalidJSON() )
370 {
371 if ( error )
372 *error = tr( "JSON value for %1 is invalid and has not been saved" ).arg( eww->field().name() );
373 return false;
374 }
375 QVariantList dstVars = QVariantList() << dst.at( eww->fieldIdx() );
376 QVariantList srcVars = QVariantList() << eww->value();
377 QList<int> fieldIndexes = QList<int>() << eww->fieldIdx();
378
379 // append additional fields
380 const QStringList additionalFields = eww->additionalFields();
381 for ( const QString &fieldName : additionalFields )
382 {
383 int idx = eww->layer()->fields().lookupField( fieldName );
384 fieldIndexes << idx;
385 dstVars << dst.at( idx );
386 }
387 srcVars.append( eww->additionalFieldValues() );
388
389 Q_ASSERT( dstVars.count() == srcVars.count() );
390
391 for ( int i = 0; i < dstVars.count(); i++ )
392 {
393
394 if ( !qgsVariantEqual( dstVars[i], srcVars[i] ) && srcVars[i].isValid() )
395 {
396 dst[fieldIndexes[i]] = srcVars[i];
397
398 doUpdate = true;
399 }
400 }
401 }
402 }
403
404 updatedFeature.setAttributes( dst );
405
406 const auto constMInterfaces = mInterfaces;
407 for ( QgsAttributeFormInterface *iface : constMInterfaces )
408 {
409 if ( !iface->acceptChanges( updatedFeature ) )
410 {
411 doUpdate = false;
412 }
413 }
414
415 if ( doUpdate )
416 {
418 {
419 mFeature = updatedFeature;
420 }
422 {
423 mFeature.setValid( true );
424 mLayer->beginEditCommand( mEditCommandMessage );
425 bool res = mLayer->addFeature( updatedFeature );
426 if ( res )
427 {
428 mFeature.setAttributes( updatedFeature.attributes() );
429 mLayer->endEditCommand();
431 changedLayer = true;
432 }
433 else
434 mLayer->destroyEditCommand();
435 }
436 else
437 {
438 mLayer->beginEditCommand( mEditCommandMessage );
439
440 QgsAttributeMap newValues;
441 QgsAttributeMap oldValues;
442
443 int n = 0;
444 for ( int i = 0; i < dst.count(); ++i )
445 {
446 if ( qgsVariantEqual( dst.at( i ), src.at( i ) ) // If field is not changed...
447 || !dst.at( i ).isValid() // or the widget returns invalid (== do not change)
448 || !fieldIsEditable( i ) ) // or the field cannot be edited ...
449 {
450 continue;
451 }
452
453 QgsDebugMsgLevel( QStringLiteral( "Updating field %1" ).arg( i ), 2 );
454 QgsDebugMsgLevel( QStringLiteral( "dst:'%1' (type:%2, isNull:%3, isValid:%4)" )
455 .arg( dst.at( i ).toString(), dst.at( i ).typeName() ).arg( QgsVariantUtils::isNull( dst.at( i ) ) ).arg( dst.at( i ).isValid() ), 2 );
456 QgsDebugMsgLevel( QStringLiteral( "src:'%1' (type:%2, isNull:%3, isValid:%4)" )
457 .arg( src.at( i ).toString(), src.at( i ).typeName() ).arg( QgsVariantUtils::isNull( src.at( i ) ) ).arg( src.at( i ).isValid() ), 2 );
458
459 newValues[i] = dst.at( i );
460 oldValues[i] = src.at( i );
461
462 n++;
463 }
464
465 success = mLayer->changeAttributeValues( mFeature.id(), newValues, oldValues );
466
467 if ( success && n > 0 )
468 {
469 mLayer->endEditCommand();
470 mFeature.setAttributes( dst );
471 changedLayer = true;
472 }
473 else
474 {
475 mLayer->destroyEditCommand();
476 }
477 }
478 }
479 }
480
481 emit featureSaved( updatedFeature );
482
483 // [MD] Refresh canvas only when absolutely necessary - it interferes with other stuff (#11361).
484 // This code should be revisited - and the signals should be fired (+ layer repainted)
485 // only when actually doing any changes. I am unsure if it is actually a good idea
486 // to call save() whenever some code asks for vector layer's modified status
487 // (which is the case when attribute table is open)
488 if ( changedLayer )
489 mLayer->triggerRepaint();
490
491 return success;
492}
493
494QgsFeature QgsAttributeForm::getUpdatedFeature() const
495{
496 // create updated Feature
497 QgsFeature updatedFeature = QgsFeature( mFeature );
498
499 QgsAttributes featureAttributes = mFeature.attributes();
500 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
501 {
502 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
503 if ( !eww )
504 continue;
505
506 QVariantList dstVars = QVariantList() << featureAttributes.at( eww->fieldIdx() );
507 QVariantList srcVars = QVariantList() << eww->value();
508 QList<int> fieldIndexes = QList<int>() << eww->fieldIdx();
509
510 // append additional fields
511 const QStringList additionalFields = eww->additionalFields();
512 for ( const QString &fieldName : additionalFields )
513 {
514 int idx = eww->layer()->fields().lookupField( fieldName );
515 fieldIndexes << idx;
516 dstVars << featureAttributes.at( idx );
517 }
518 srcVars.append( eww->additionalFieldValues() );
519
520 Q_ASSERT( dstVars.count() == srcVars.count() );
521
522 for ( int i = 0; i < dstVars.count(); i++ )
523 {
524 if ( !qgsVariantEqual( dstVars[i], srcVars[i] ) && srcVars[i].isValid() )
525 featureAttributes[fieldIndexes[i]] = srcVars[i];
526 }
527 }
528 updatedFeature.setAttributes( featureAttributes );
529
530 return updatedFeature;
531}
532
533void QgsAttributeForm::updateValuesDependencies( const int originIdx )
534{
535 updateValuesDependenciesDefaultValues( originIdx );
536 updateValuesDependenciesVirtualFields( originIdx );
537}
538
539void QgsAttributeForm::updateValuesDependenciesDefaultValues( const int originIdx )
540{
541 if ( !mDefaultValueDependencies.contains( originIdx ) )
542 return;
543
544 if ( !mFeature.isValid()
546 return;
547
548 // create updated Feature
549 QgsFeature updatedFeature = getUpdatedFeature();
550
551 // go through depending fields and update the fields with defaultexpression
552 QList<QgsWidgetWrapper *> relevantWidgets = mDefaultValueDependencies.values( originIdx );
553 for ( QgsWidgetWrapper *ww : std::as_const( relevantWidgets ) )
554 {
555 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
556 if ( eww )
557 {
558 // Update only on form opening (except when applyOnUpdate is activated)
559 if ( mValuesInitialized && !eww->field().defaultValueDefinition().applyOnUpdate() )
560 continue;
561
562 // Update only when mMode is AddFeatureMode (except when applyOnUpdate is activated)
564 {
565 continue;
566 }
567
568 //do not update when this widget is already updating (avoid recursions)
569 if ( mAlreadyUpdatedFields.contains( eww->fieldIdx() ) )
570 continue;
571
572 QgsExpressionContext context = createExpressionContext( updatedFeature );
573 const QVariant value = mLayer->defaultValue( eww->fieldIdx(), updatedFeature, &context );
574 eww->setValue( value );
575 mCurrentFormFeature.setAttribute( eww->field().name(), value );
576 }
577 }
578}
579
580void QgsAttributeForm::updateValuesDependenciesVirtualFields( const int originIdx )
581{
582 if ( !mVirtualFieldsDependencies.contains( originIdx ) )
583 return;
584
585 if ( !mFeature.isValid() )
586 return;
587
588 // create updated Feature
589 QgsFeature updatedFeature = getUpdatedFeature();
590
591 // go through depending fields and update the virtual field with its expression
592 const QList<QgsWidgetWrapper *> relevantWidgets = mVirtualFieldsDependencies.values( originIdx );
593 for ( QgsWidgetWrapper *ww : relevantWidgets )
594 {
595 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
596 if ( !eww )
597 continue;
598
599 //do not update when this widget is already updating (avoid recursions)
600 if ( mAlreadyUpdatedFields.contains( eww->fieldIdx() ) )
601 continue;
602
603 // Update value
604 QgsExpressionContext context = createExpressionContext( updatedFeature );
605 QgsExpression exp( mLayer->expressionField( eww->fieldIdx() ) );
606 const QVariant value = exp.evaluate( &context );
607 updatedFeature.setAttribute( eww->fieldIdx(), value );
608 eww->setValue( value );
609 }
610}
611
612void QgsAttributeForm::updateRelatedLayerFields()
613{
614 // Synchronize dependencies
615 updateRelatedLayerFieldsDependencies();
616
617 if ( mRelatedLayerFieldsDependencies.isEmpty() )
618 return;
619
620 if ( !mFeature.isValid() )
621 return;
622
623 // create updated Feature
624 QgsFeature updatedFeature = getUpdatedFeature();
625
626 // go through depending fields and update the fields with virtual field
627 const QSet<QgsEditorWidgetWrapper *> relevantWidgets = mRelatedLayerFieldsDependencies;
628 for ( QgsEditorWidgetWrapper *eww : relevantWidgets )
629 {
630 //do not update when this widget is already updating (avoid recursions)
631 if ( mAlreadyUpdatedFields.contains( eww->fieldIdx() ) )
632 continue;
633
634 // Update value
635 QgsExpressionContext context = createExpressionContext( updatedFeature );
636 QgsExpression exp( mLayer->expressionField( eww->fieldIdx() ) );
637 QVariant value = exp.evaluate( &context );
638 eww->setValue( value );
639 }
640}
641
642void QgsAttributeForm::resetMultiEdit( bool promptToSave )
643{
644 if ( promptToSave )
645 save();
646
647 mUnsavedMultiEditChanges = false;
649}
650
651void QgsAttributeForm::multiEditMessageClicked( const QString &link )
652{
653 clearMultiEditMessages();
654 resetMultiEdit( link == QLatin1String( "#apply" ) );
655}
656
657void QgsAttributeForm::filterTriggered()
658{
659 QString filter = createFilterExpression();
660 emit filterExpressionSet( filter, ReplaceFilter );
661 if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
663}
664
665void QgsAttributeForm::searchZoomTo()
666{
667 QString filter = createFilterExpression();
668 if ( filter.isEmpty() )
669 return;
670
671 emit zoomToFeatures( filter );
672}
673
674void QgsAttributeForm::searchFlash()
675{
676 QString filter = createFilterExpression();
677 if ( filter.isEmpty() )
678 return;
679
680 emit flashFeatures( filter );
681}
682
683void QgsAttributeForm::filterAndTriggered()
684{
685 QString filter = createFilterExpression();
686 if ( filter.isEmpty() )
687 return;
688
689 if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
691 emit filterExpressionSet( filter, FilterAnd );
692}
693
694void QgsAttributeForm::filterOrTriggered()
695{
696 QString filter = createFilterExpression();
697 if ( filter.isEmpty() )
698 return;
699
700 if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
702 emit filterExpressionSet( filter, FilterOr );
703}
704
705void QgsAttributeForm::pushSelectedFeaturesMessage()
706{
707 int count = mLayer->selectedFeatureCount();
708 if ( count > 0 )
709 {
710 mMessageBar->pushMessage( QString(),
711 tr( "%n matching feature(s) selected", "matching features", count ),
712 Qgis::MessageLevel::Info );
713 }
714 else
715 {
716 mMessageBar->pushMessage( QString(),
717 tr( "No matching features found" ),
718 Qgis::MessageLevel::Info );
719 }
720}
721
722void QgsAttributeForm::displayWarning( const QString &message )
723{
724 mMessageBar->pushMessage( QString(),
725 message,
726 Qgis::MessageLevel::Warning );
727}
728
729void QgsAttributeForm::runSearchSelect( Qgis::SelectBehavior behavior )
730{
731 QString filter = createFilterExpression();
732 if ( filter.isEmpty() )
733 return;
734
735 mLayer->selectByExpression( filter, behavior );
736 pushSelectedFeaturesMessage();
737 if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
739}
740
741void QgsAttributeForm::searchSetSelection()
742{
743 runSearchSelect( Qgis::SelectBehavior::SetSelection );
744}
745
746void QgsAttributeForm::searchAddToSelection()
747{
748 runSearchSelect( Qgis::SelectBehavior::AddToSelection );
749}
750
751void QgsAttributeForm::searchRemoveFromSelection()
752{
754}
755
756void QgsAttributeForm::searchIntersectSelection()
757{
759}
760
761bool QgsAttributeForm::saveMultiEdits()
762{
763 //find changed attributes
764 QgsAttributeMap newAttributeValues;
765 const QList<int> fieldIndexes = mFormEditorWidgets.uniqueKeys();
766 mFormEditorWidgets.constBegin();
767 for ( int fieldIndex : fieldIndexes )
768 {
769 const QList<QgsAttributeFormEditorWidget *> widgets = mFormEditorWidgets.values( fieldIndex );
770 if ( !widgets.first()->hasChanged() )
771 continue;
772
773 if ( !widgets.first()->currentValue().isValid() // if the widget returns invalid (== do not change)
774 || !fieldIsEditable( fieldIndex ) ) // or the field cannot be edited ...
775 {
776 continue;
777 }
778
779 // let editor know we've accepted the changes
780 for ( QgsAttributeFormEditorWidget *widget : widgets )
781 widget->changesCommitted();
782
783 newAttributeValues.insert( fieldIndex, widgets.first()->currentValue() );
784 }
785
786 if ( newAttributeValues.isEmpty() )
787 {
788 //nothing to change
789 return true;
790 }
791
792#if 0
793 // prompt for save
794 int res = QMessageBox::information( this, tr( "Multiedit Attributes" ),
795 tr( "Edits will be applied to all selected features." ), QMessageBox::Ok | QMessageBox::Cancel );
796 if ( res != QMessageBox::Ok )
797 {
798 resetMultiEdit();
799 return false;
800 }
801#endif
802
803 mLayer->beginEditCommand( tr( "Updated multiple feature attributes" ) );
804
805 bool success = true;
806
807 const auto constMultiEditFeatureIds = mMultiEditFeatureIds;
808 for ( QgsFeatureId fid : constMultiEditFeatureIds )
809 {
810 QgsAttributeMap::const_iterator aIt = newAttributeValues.constBegin();
811 for ( ; aIt != newAttributeValues.constEnd(); ++aIt )
812 {
813 success &= mLayer->changeAttributeValue( fid, aIt.key(), aIt.value() );
814 }
815 }
816
817 clearMultiEditMessages();
818 if ( success )
819 {
820 mLayer->endEditCommand();
821 mLayer->triggerRepaint();
822 mMultiEditMessageBarItem = new QgsMessageBarItem( tr( "Attribute changes for multiple features applied." ), Qgis::MessageLevel::Success, -1 );
823 }
824 else
825 {
826 mLayer->destroyEditCommand();
827 mMultiEditMessageBarItem = new QgsMessageBarItem( tr( "Changes could not be applied." ), Qgis::MessageLevel::Warning, 0 );
828 }
829
830 if ( !mButtonBox->isVisible() )
831 mMessageBar->pushItem( mMultiEditMessageBarItem );
832 return success;
833}
834
836{
837 return saveWithDetails( nullptr );
838}
839
841{
842 if ( error )
843 error->clear();
844
845 if ( mIsSaving )
846 return true;
847
848 if ( mContext.formMode() == QgsAttributeEditorContext::Embed && !mValidConstraints )
849 {
850 // the feature isn't saved (as per the warning provided), but we return true
851 // so switching features still works
852 return true;
853 }
854
855 for ( QgsWidgetWrapper *wrapper : std::as_const( mWidgets ) )
856 {
857 wrapper->notifyAboutToSave();
858 }
859
860 // only do the dirty checks when editing an existing feature - for new
861 // features we need to add them even if the attributes are unchanged from the initial
862 // default values
863 switch ( mMode )
864 {
869 if ( !mDirty )
870 return true;
871 break;
872
876 break;
877 }
878
879 mIsSaving = true;
880
881 bool success = true;
882
883 emit beforeSave( success );
884
885 // Somebody wants to prevent this form from saving
886 if ( !success )
887 return false;
888
889 switch ( mMode )
890 {
897 success = saveEdits( error );
898 break;
899
901 success = saveMultiEdits();
902 break;
903 }
904
905 mIsSaving = false;
906 mUnsavedMultiEditChanges = false;
907 mDirty = false;
908
909 return success;
910}
911
912
914{
915 mValuesInitialized = false;
916 const auto constMWidgets = mWidgets;
917 for ( QgsWidgetWrapper *ww : constMWidgets )
918 {
919 ww->setFeature( mFeature );
920 }
921
922 // Update dependent virtual fields (not default values / not referencing layer values)
923 for ( QgsWidgetWrapper *ww : constMWidgets )
924 {
925 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
926 if ( !eww )
927 continue;
928
929 // Append field index here, so it's not updated recursively
930 mAlreadyUpdatedFields.append( eww->fieldIdx() );
931 updateValuesDependenciesVirtualFields( eww->fieldIdx() );
932 mAlreadyUpdatedFields.removeAll( eww->fieldIdx() );
933 }
934
935 mValuesInitialized = true;
936 mDirty = false;
937}
938
940{
941 const auto widgets { findChildren< QgsAttributeFormEditorWidget * >() };
942 for ( QgsAttributeFormEditorWidget *w : widgets )
943 {
944 w->resetSearch();
945 }
946}
947
948void QgsAttributeForm::clearMultiEditMessages()
949{
950 if ( mMultiEditUnsavedMessageBarItem )
951 {
952 if ( !mButtonBox->isVisible() )
953 mMessageBar->popWidget( mMultiEditUnsavedMessageBarItem );
954 mMultiEditUnsavedMessageBarItem = nullptr;
955 }
956 if ( mMultiEditMessageBarItem )
957 {
958 if ( !mButtonBox->isVisible() )
959 mMessageBar->popWidget( mMultiEditMessageBarItem );
960 mMultiEditMessageBarItem = nullptr;
961 }
962}
963
964QString QgsAttributeForm::createFilterExpression() const
965{
966 QStringList filters;
967 for ( QgsAttributeFormWidget *w : std::as_const( mFormWidgets ) )
968 {
969 QString filter = w->currentFilterExpression();
970 if ( !filter.isEmpty() )
971 filters << filter;
972 }
973
974 if ( filters.isEmpty() )
975 return QString();
976
977 QString filter = filters.join( QLatin1String( ") AND (" ) ).prepend( '(' ).append( ')' );
978 return filter;
979}
980
981QgsExpressionContext QgsAttributeForm::createExpressionContext( const QgsFeature &feature ) const
982{
983 QgsExpressionContext context;
986 if ( mExtraContextScope )
987 context.appendScope( new QgsExpressionContextScope( *mExtraContextScope.get() ) );
988 context.setFeature( feature );
989 return context;
990}
991
992
993void QgsAttributeForm::onAttributeChanged( const QVariant &value, const QVariantList &additionalFieldValues )
994{
995 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( sender() );
996 Q_ASSERT( eww );
997
998 bool signalEmitted = false;
999
1000 if ( mValuesInitialized )
1001 mDirty = true;
1002
1003 mCurrentFormFeature.setAttribute( eww->field().name(), value );
1004
1005 switch ( mMode )
1006 {
1011 {
1013 emit attributeChanged( eww->field().name(), value );
1015 emit widgetValueChanged( eww->field().name(), value, !mIsSettingFeature );
1016
1017 // also emit the signal for additional values
1018 const QStringList additionalFields = eww->additionalFields();
1019 for ( int i = 0; i < additionalFields.count(); i++ )
1020 {
1021 const QString fieldName = additionalFields.at( i );
1022 const QVariant value = additionalFieldValues.at( i );
1023 emit widgetValueChanged( fieldName, value, !mIsSettingFeature );
1024 }
1025
1026 signalEmitted = true;
1027
1028 if ( mValuesInitialized )
1029 updateJoinedFields( *eww );
1030
1031 break;
1032 }
1034 {
1035 if ( !mIsSettingMultiEditFeatures )
1036 {
1037 mUnsavedMultiEditChanges = true;
1038
1039 QLabel *msgLabel = new QLabel( tr( "Unsaved multiedit changes: <a href=\"#apply\">apply changes</a> or <a href=\"#reset\">reset changes</a>." ), mMessageBar );
1040 msgLabel->setAlignment( Qt::AlignLeft | Qt::AlignVCenter );
1041 msgLabel->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );
1042 connect( msgLabel, &QLabel::linkActivated, this, &QgsAttributeForm::multiEditMessageClicked );
1043 clearMultiEditMessages();
1044
1045 mMultiEditUnsavedMessageBarItem = new QgsMessageBarItem( msgLabel, Qgis::MessageLevel::Warning );
1046 if ( !mButtonBox->isVisible() )
1047 mMessageBar->pushItem( mMultiEditUnsavedMessageBarItem );
1048
1049 emit widgetValueChanged( eww->field().name(), value, false );
1050 signalEmitted = true;
1051 }
1052 break;
1053 }
1056 //nothing to do
1057 break;
1058 }
1059
1060 // Update other widgets pointing to the same field, required to happen now to insure
1061 // currentFormValuesFeature() gets the right value when processing constraints
1062 const QList<QgsAttributeFormEditorWidget *> formEditorWidgets = mFormEditorWidgets.values( eww->fieldIdx() );
1063 for ( QgsAttributeFormEditorWidget *formEditorWidget : formEditorWidgets )
1064 {
1065 if ( formEditorWidget->editorWidget() == eww )
1066 continue;
1067
1068 // formEditorWidget and eww points to the same field, so block signals
1069 // as there is no need to handle valueChanged again for each duplicate
1070 whileBlocking( formEditorWidget->editorWidget() )->setValue( value );
1071 }
1072
1073 updateConstraints( eww );
1074
1075 // Update dependent fields (only if form is not initializing)
1076 if ( mValuesInitialized )
1077 {
1078 //append field index here, so it's not updated recursive
1079 mAlreadyUpdatedFields.append( eww->fieldIdx() );
1080 updateValuesDependencies( eww->fieldIdx() );
1081 mAlreadyUpdatedFields.removeAll( eww->fieldIdx() );
1082 }
1083
1084 // Updates expression controlled labels and editable state
1085 updateLabels();
1086 updateEditableState();
1087
1088 if ( !signalEmitted )
1089 {
1091 emit attributeChanged( eww->field().name(), value );
1093 bool attributeHasChanged = !mIsSettingFeature;
1095 attributeHasChanged &= !mIsSettingMultiEditFeatures;
1096
1097 emit widgetValueChanged( eww->field().name(), value, attributeHasChanged );
1098 }
1099}
1100
1101void QgsAttributeForm::updateAllConstraints()
1102{
1103 const auto constMWidgets = mWidgets;
1104 for ( QgsWidgetWrapper *ww : constMWidgets )
1105 {
1106 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1107 if ( eww )
1108 updateConstraints( eww );
1109 }
1110}
1111
1112void QgsAttributeForm::updateConstraints( QgsEditorWidgetWrapper *eww )
1113{
1114 // get the current feature set in the form
1115 QgsFeature ft;
1116 if ( currentFormValuesFeature( ft ) )
1117 {
1118 // if the layer is NOT being edited then we only check layer based constraints, and not
1119 // any constraints enforced by the provider. Because:
1120 // 1. we want to keep browsing features nice and responsive. It's nice to give feedback as to whether
1121 // the value checks out, but not if it's too slow to do so. Some constraints (e.g., unique) can be
1122 // expensive to test. A user can freely remove a layer-based constraint if it proves to be too slow
1123 // to test, but they are unlikely to have any control over provider-side constraints
1124 // 2. the provider has already accepted the value, so presumably it doesn't violate the constraint
1125 // and there's no point rechecking!
1126
1127 // update eww constraint
1128 updateConstraint( ft, eww );
1129
1130 // update eww dependencies constraint
1131 const QList<QgsEditorWidgetWrapper *> deps = constraintDependencies( eww );
1132
1133 for ( QgsEditorWidgetWrapper *depsEww : deps )
1134 updateConstraint( ft, depsEww );
1135
1136 // sync OK button status
1137 synchronizeState();
1138
1139 QgsExpressionContext context = createExpressionContext( ft );
1140
1141 // Recheck visibility/collapsed state for all containers which are controlled by this value
1142 const QVector<ContainerInformation *> infos = mContainerInformationDependency.value( eww->field().name() );
1143 for ( ContainerInformation *info : infos )
1144 {
1145 info->apply( &context );
1146 }
1147 }
1148}
1149
1150void QgsAttributeForm::updateContainersVisibility()
1151{
1152 QgsExpressionContext context = createExpressionContext( mFeature );
1153
1154 const QVector<ContainerInformation *> infos = mContainerVisibilityCollapsedInformation;
1155
1156 for ( ContainerInformation *info : infos )
1157 {
1158 info->apply( &context );
1159 }
1160
1161 // Update the constraints if not in multi edit, because
1162 // when mode changes to multi edit, constraints have been already
1163 // updated and a further update will use current form feature values,
1164 // possibly empty for mixed values, leading to false positive
1165 // constraints violations.
1166 if ( mMode != QgsAttributeEditorContext::Mode::MultiEditMode )
1167 {
1168 updateAllConstraints();
1169 }
1170}
1171
1172void QgsAttributeForm::updateConstraint( const QgsFeature &ft, QgsEditorWidgetWrapper *eww )
1173{
1174
1176
1177 if ( eww->layer()->fields().fieldOrigin( eww->fieldIdx() ) == QgsFields::OriginJoin )
1178 {
1179 int srcFieldIdx;
1180 const QgsVectorLayerJoinInfo *info = eww->layer()->joinBuffer()->joinForFieldIndex( eww->fieldIdx(), eww->layer()->fields(), srcFieldIdx );
1181
1182 if ( info && info->joinLayer() && info->isDynamicFormEnabled() )
1183 {
1184 if ( mJoinedFeatures.contains( info ) )
1185 {
1186 eww->updateConstraint( info->joinLayer(), srcFieldIdx, mJoinedFeatures[info], constraintOrigin );
1187 return;
1188 }
1189 else // if we are here, it means there's not joined field for this feature
1190 {
1191 eww->updateConstraint( QgsFeature() );
1192 return;
1193 }
1194 }
1195 }
1196 // default constraint update
1197 eww->updateConstraint( ft, constraintOrigin );
1198
1199}
1200
1201void QgsAttributeForm::updateLabels()
1202{
1203 if ( ! mLabelDataDefinedProperties.isEmpty() )
1204 {
1205 QgsFeature currentFeature;
1206 if ( currentFormValuesFeature( currentFeature ) )
1207 {
1208 QgsExpressionContext context = createExpressionContext( currentFeature );
1209
1210 for ( auto it = mLabelDataDefinedProperties.constBegin() ; it != mLabelDataDefinedProperties.constEnd(); ++it )
1211 {
1212 QLabel *label { it.key() };
1213 bool ok;
1214 const QString value { it->valueAsString( context, QString(), &ok ) };
1215 if ( ok && ! value.isEmpty() )
1216 {
1217 label->setText( value );
1218 }
1219 }
1220 }
1221 }
1222}
1223
1224void QgsAttributeForm::updateEditableState()
1225{
1226 if ( ! mEditableDataDefinedProperties.isEmpty() )
1227 {
1228 QgsFeature currentFeature;
1229 if ( currentFormValuesFeature( currentFeature ) )
1230 {
1231 QgsExpressionContext context = createExpressionContext( currentFeature );
1232
1233 for ( auto it = mEditableDataDefinedProperties.constBegin() ; it != mEditableDataDefinedProperties.constEnd(); ++it )
1234 {
1235 QWidget *w { it.key() };
1236 bool ok;
1237 const bool isEditable { it->valueAsBool( context, true, &ok ) && mLayer && mLayer->isEditable() }; // *NOPAD*
1238 if ( ok )
1239 {
1240 QgsAttributeFormEditorWidget *editorWidget { qobject_cast<QgsAttributeFormEditorWidget *>( w ) };
1241 if ( editorWidget )
1242 {
1243 editorWidget->editorWidget()->setEnabled( isEditable );
1244 }
1245 else
1246 {
1247 w->setEnabled( isEditable );
1248 }
1249 }
1250 }
1251 }
1252 }
1253}
1254
1255bool QgsAttributeForm::currentFormValuesFeature( QgsFeature &feature )
1256{
1257 bool rc = true;
1258 feature = QgsFeature( mFeature );
1260
1261 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
1262 {
1263 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1264
1265 if ( !eww )
1266 continue;
1267
1268 if ( dst.count() > eww->fieldIdx() )
1269 {
1270 QVariantList dstVars = QVariantList() << dst.at( eww->fieldIdx() );
1271 QVariantList srcVars = QVariantList() << eww->value();
1272 QList<int> fieldIndexes = QList<int>() << eww->fieldIdx();
1273
1274 // append additional fields
1275 const QStringList additionalFields = eww->additionalFields();
1276 for ( const QString &fieldName : additionalFields )
1277 {
1278 int idx = eww->layer()->fields().lookupField( fieldName );
1279 fieldIndexes << idx;
1280 dstVars << dst.at( idx );
1281 }
1282 srcVars.append( eww->additionalFieldValues() );
1283
1284 Q_ASSERT( dstVars.count() == srcVars.count() );
1285
1286 for ( int i = 0; i < dstVars.count(); i++ )
1287 {
1288 // need to check dstVar.isNull() != srcVar.isNull()
1289 // otherwise if dstVar=NULL and scrVar=0, then dstVar = srcVar
1290 if ( ( !qgsVariantEqual( dstVars[i], srcVars[i] ) || QgsVariantUtils::isNull( dstVars[i] ) != QgsVariantUtils::isNull( srcVars[i] ) ) && srcVars[i].isValid() )
1291 {
1292 dst[fieldIndexes[i]] = srcVars[i];
1293 }
1294 }
1295 }
1296 else
1297 {
1298 rc = false;
1299 break;
1300 }
1301 }
1302
1303 feature.setAttributes( dst );
1304
1305 return rc;
1306}
1307
1308
1309void QgsAttributeForm::registerContainerInformation( QgsAttributeForm::ContainerInformation *info )
1310{
1311 mContainerVisibilityCollapsedInformation.append( info );
1312
1313 const QSet<QString> referencedColumns = info->expression.referencedColumns().unite( info->collapsedExpression.referencedColumns() );
1314
1315 for ( const QString &col : referencedColumns )
1316 {
1317 mContainerInformationDependency[ col ].append( info );
1318 }
1319}
1320
1321bool QgsAttributeForm::currentFormValidConstraints( QStringList &invalidFields, QStringList &descriptions ) const
1322{
1323 bool valid{ true };
1324
1325 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
1326 {
1327 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1328 if ( eww )
1329 {
1330 if ( ! eww->isValidConstraint() )
1331 {
1332 invalidFields.append( eww->field().displayName() );
1333
1334 descriptions.append( eww->constraintFailureReason() );
1335
1336 if ( eww->isBlockingCommit() )
1337 valid = false; // continue to get all invalid fields
1338 }
1339 }
1340 }
1341
1342 return valid;
1343}
1344
1345bool QgsAttributeForm::currentFormValidHardConstraints( QStringList &invalidFields, QStringList &descriptions ) const
1346{
1347 bool valid{ true };
1348
1349 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
1350 {
1351 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1352 if ( eww )
1353 {
1354 if ( eww->isBlockingCommit() )
1355 {
1356 invalidFields.append( eww->field().displayName() );
1357 descriptions.append( eww->constraintFailureReason() );
1358 valid = false; // continue to get all invalid fields
1359 }
1360 }
1361 }
1362
1363 return valid;
1364}
1365
1366void QgsAttributeForm::onAttributeAdded( int idx )
1367{
1368 mPreventFeatureRefresh = false;
1369 if ( mFeature.isValid() )
1370 {
1371 QgsAttributes attrs = mFeature.attributes();
1372 attrs.insert( idx, QVariant( layer()->fields().at( idx ).type() ) );
1373 mFeature.setFields( layer()->fields() );
1374 mFeature.setAttributes( attrs );
1375 }
1376 init();
1377 setFeature( mFeature );
1378}
1379
1380void QgsAttributeForm::onAttributeDeleted( int idx )
1381{
1382 mPreventFeatureRefresh = false;
1383 if ( mFeature.isValid() )
1384 {
1385 QgsAttributes attrs = mFeature.attributes();
1386 attrs.remove( idx );
1387 mFeature.setFields( layer()->fields() );
1388 mFeature.setAttributes( attrs );
1389 }
1390 init();
1391 setFeature( mFeature );
1392}
1393
1394void QgsAttributeForm::onRelatedFeaturesChanged()
1395{
1396 updateRelatedLayerFields();
1397}
1398
1399void QgsAttributeForm::onUpdatedFields()
1400{
1401 mPreventFeatureRefresh = false;
1402 if ( mFeature.isValid() )
1403 {
1404 QgsAttributes attrs( layer()->fields().size() );
1405 for ( int i = 0; i < layer()->fields().size(); i++ )
1406 {
1407 int idx = mFeature.fields().indexFromName( layer()->fields().at( i ).name() );
1408 if ( idx != -1 )
1409 {
1410 attrs[i] = mFeature.attributes().at( idx );
1411 if ( mFeature.attributes().at( idx ).type() != layer()->fields().at( i ).type() )
1412 {
1413 attrs[i].convert( layer()->fields().at( i ).type() );
1414 }
1415 }
1416 else
1417 {
1418 attrs[i] = QVariant( layer()->fields().at( i ).type() );
1419 }
1420 }
1421 mFeature.setFields( layer()->fields() );
1422 mFeature.setAttributes( attrs );
1423 }
1424 init();
1425 setFeature( mFeature );
1426}
1427
1428void QgsAttributeForm::onConstraintStatusChanged( const QString &constraint,
1429 const QString &description, const QString &err, QgsEditorWidgetWrapper::ConstraintResult result )
1430{
1431 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( sender() );
1432 Q_ASSERT( eww );
1433
1434 const QList<QgsAttributeFormEditorWidget *> formEditorWidgets = mFormEditorWidgets.values( eww->fieldIdx() );
1435
1436 for ( QgsAttributeFormEditorWidget *formEditorWidget : formEditorWidgets )
1437 {
1438 formEditorWidget->setConstraintStatus( constraint, description, err, result );
1439 if ( formEditorWidget->editorWidget() != eww )
1440 {
1441 formEditorWidget->editorWidget()->updateConstraint( result, err );
1442 }
1443 }
1444}
1445
1446QList<QgsEditorWidgetWrapper *> QgsAttributeForm::constraintDependencies( QgsEditorWidgetWrapper *w )
1447{
1448 QList<QgsEditorWidgetWrapper *> wDeps;
1449 QString name = w->field().name();
1450
1451 // for each widget in the current form
1452 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
1453 {
1454 // get the wrapper
1455 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1456 if ( eww )
1457 {
1458 // compare name to not compare w to itself
1459 QString ewwName = eww->field().name();
1460 if ( name != ewwName )
1461 {
1462 // get expression and referencedColumns
1463 QgsExpression expr = eww->layer()->fields().at( eww->fieldIdx() ).constraints().constraintExpression();
1464
1465 const auto referencedColumns = expr.referencedColumns();
1466
1467 for ( const QString &colName : referencedColumns )
1468 {
1469 if ( name == colName )
1470 {
1471 wDeps.append( eww );
1472 break;
1473 }
1474 }
1475 }
1476 }
1477 }
1478
1479 return wDeps;
1480}
1481
1482QgsRelationWidgetWrapper *QgsAttributeForm::setupRelationWidgetWrapper( const QgsRelation &rel, const QgsAttributeEditorContext &context )
1483{
1484 return setupRelationWidgetWrapper( QString(), rel, context );
1485}
1486
1487QgsRelationWidgetWrapper *QgsAttributeForm::setupRelationWidgetWrapper( const QString &relationWidgetTypeId, const QgsRelation &rel, const QgsAttributeEditorContext &context )
1488{
1489 QgsRelationWidgetWrapper *rww = new QgsRelationWidgetWrapper( relationWidgetTypeId, mLayer, rel, nullptr, this );
1490 const QVariantMap config = mLayer->editFormConfig().widgetConfig( rel.id() );
1491 rww->setConfig( config );
1492 rww->setContext( context );
1493
1494 return rww;
1495}
1496
1497void QgsAttributeForm::preventFeatureRefresh()
1498{
1499 mPreventFeatureRefresh = true;
1500}
1501
1503{
1504 if ( mPreventFeatureRefresh || mLayer->isEditable() || !mFeature.isValid() )
1505 return;
1506
1507 // reload feature if layer changed although not editable
1508 // (datasource probably changed bypassing QgsVectorLayer)
1509 if ( !mLayer->getFeatures( QgsFeatureRequest().setFilterFid( mFeature.id() ) ).nextFeature( mFeature ) )
1510 return;
1511
1512 init();
1513 setFeature( mFeature );
1514}
1515
1516void QgsAttributeForm::parentFormValueChanged( const QString &attribute, const QVariant &newValue )
1517{
1518 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
1519 {
1520 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1521 if ( eww )
1522 {
1523 eww->parentFormValueChanged( attribute, newValue );
1524 }
1525 }
1526}
1527
1529{
1530 return mNeedsGeometry;
1531}
1532
1533void QgsAttributeForm::synchronizeState()
1534{
1535 bool isEditable = ( mFeature.isValid()
1537 || mMode == QgsAttributeEditorContext::MultiEditMode ) && mLayer->isEditable();
1538
1539 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
1540 {
1541
1542 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
1543 if ( eww )
1544 {
1545 const QList<QgsAttributeFormEditorWidget *> formWidgets = mFormEditorWidgets.values( eww->fieldIdx() );
1546
1547 for ( QgsAttributeFormEditorWidget *formWidget : formWidgets )
1548 formWidget->setConstraintResultVisible( isEditable );
1549
1550 eww->setConstraintResultVisible( isEditable );
1551
1552 bool enabled = isEditable && fieldIsEditable( eww->fieldIdx() );
1553 ww->setEnabled( enabled );
1554
1555 updateIcon( eww );
1556 }
1557 else // handle QgsWidgetWrapper different than QgsEditorWidgetWrapper
1558 {
1559 ww->setEnabled( isEditable );
1560 }
1561
1562 }
1563
1564
1566 {
1567 if ( mMode == QgsAttributeEditorContext::Mode::MultiEditMode && mLayer->selectedFeatureCount() == 0 )
1568 {
1569 isEditable = false;
1570 if ( mConstraintsFailMessageBarItem )
1571 {
1572 mMessageBar->popWidget( mConstraintsFailMessageBarItem );
1573 }
1574 mConstraintsFailMessageBarItem = new QgsMessageBarItem( tr( "Multi edit mode requires at least one selected feature." ), Qgis::MessageLevel::Info, -1 );
1575 mMessageBar->pushItem( mConstraintsFailMessageBarItem );
1576 }
1577 else
1578 {
1579 QStringList invalidFields, descriptions;
1580 mValidConstraints = currentFormValidHardConstraints( invalidFields, descriptions );
1581
1582 if ( isEditable && mContext.formMode() == QgsAttributeEditorContext::Embed )
1583 {
1584 if ( !mValidConstraints && !mConstraintsFailMessageBarItem )
1585 {
1586 mConstraintsFailMessageBarItem = 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 );
1587 mMessageBar->pushItem( mConstraintsFailMessageBarItem );
1588 }
1589 else if ( mValidConstraints && mConstraintsFailMessageBarItem )
1590 {
1591 mMessageBar->popWidget( mConstraintsFailMessageBarItem );
1592 mConstraintsFailMessageBarItem = nullptr;
1593 }
1594 }
1595 else if ( mConstraintsFailMessageBarItem )
1596 {
1597 mMessageBar->popWidget( mConstraintsFailMessageBarItem );
1598 mConstraintsFailMessageBarItem = nullptr;
1599 }
1600
1601 isEditable = isEditable & mValidConstraints;
1602 }
1603 }
1604
1605 // change OK button status
1606 QPushButton *okButton = mButtonBox->button( QDialogButtonBox::Ok );
1607 if ( okButton )
1608 okButton->setEnabled( isEditable );
1609}
1610
1611void QgsAttributeForm::init()
1612{
1613 QApplication::setOverrideCursor( QCursor( Qt::WaitCursor ) );
1614
1615 // Cleanup of any previously shown widget, we start from scratch
1616 QWidget *formWidget = nullptr;
1617 mNeedsGeometry = false;
1618
1619 bool buttonBoxVisible = true;
1620 // Cleanup button box but preserve visibility
1621 if ( mButtonBox )
1622 {
1623 buttonBoxVisible = mButtonBox->isVisible();
1624 delete mButtonBox;
1625 mButtonBox = nullptr;
1626 }
1627
1628 if ( mSearchButtonBox )
1629 {
1630 delete mSearchButtonBox;
1631 mSearchButtonBox = nullptr;
1632 }
1633
1634 qDeleteAll( mWidgets );
1635 mWidgets.clear();
1636
1637 while ( QWidget *w = this->findChild<QWidget *>() )
1638 {
1639 delete w;
1640 }
1641 delete layout();
1642
1643 QVBoxLayout *vl = new QVBoxLayout();
1644 vl->setContentsMargins( 0, 0, 0, 0 );
1645 mMessageBar = new QgsMessageBar( this );
1646 mMessageBar->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );
1647 vl->addWidget( mMessageBar );
1648
1649 setLayout( vl );
1650
1651 // Get a layout
1652 QGridLayout *layout = new QGridLayout();
1653 QWidget *container = new QWidget();
1654 container->setLayout( layout );
1655 vl->addWidget( container );
1656
1657 mFormEditorWidgets.clear();
1658 mFormWidgets.clear();
1659
1660 // a bar to warn the user with non-blocking messages
1661 setContentsMargins( 0, 0, 0, 0 );
1662
1663 // Try to load Ui-File for layout
1664 if ( mContext.allowCustomUi() && mLayer->editFormConfig().layout() == Qgis::AttributeFormLayout::UiFile &&
1665 !mLayer->editFormConfig().uiForm().isEmpty() )
1666 {
1667 QgsDebugMsgLevel( QStringLiteral( "loading form: %1" ).arg( mLayer->editFormConfig().uiForm() ), 2 );
1668 const QString path = mLayer->editFormConfig().uiForm();
1670 if ( file && file->open( QFile::ReadOnly ) )
1671 {
1672 QUiLoader loader;
1673
1674 QFileInfo fi( file->fileName() );
1675 loader.setWorkingDirectory( fi.dir() );
1676 formWidget = loader.load( file, this );
1677 if ( formWidget )
1678 {
1679 formWidget->setWindowFlags( Qt::Widget );
1680 layout->addWidget( formWidget );
1681 formWidget->show();
1682 file->close();
1683 mButtonBox = findChild<QDialogButtonBox *>();
1684 createWrappers();
1685
1686 formWidget->installEventFilter( this );
1687 }
1688 }
1689 }
1690
1691 QgsTabWidget *tabWidget = nullptr;
1692
1693 // Tab layout
1694 if ( !formWidget && mLayer->editFormConfig().layout() == Qgis::AttributeFormLayout::DragAndDrop )
1695 {
1696 int row = 0;
1697 int column = 0;
1698 int columnCount = 1;
1699 bool hasRootFields = false;
1700 bool addSpacer = true;
1701
1702 const QList<QgsAttributeEditorElement *> tabs = mLayer->editFormConfig().tabs();
1703
1704 for ( QgsAttributeEditorElement *widgDef : tabs )
1705 {
1706 if ( widgDef->type() == Qgis::AttributeEditorType::Container )
1707 {
1708 QgsAttributeEditorContainer *containerDef = dynamic_cast<QgsAttributeEditorContainer *>( widgDef );
1709 if ( !containerDef )
1710 continue;
1711
1712 switch ( containerDef->type() )
1713 {
1715 {
1716 tabWidget = nullptr;
1717 WidgetInfo widgetInfo = createWidgetFromDef( widgDef, formWidget, mLayer, mContext );
1718 if ( widgetInfo.labelStyle.overrideColor )
1719 {
1720 if ( widgetInfo.labelStyle.color.isValid() )
1721 {
1722 widgetInfo.widget->setStyleSheet( QStringLiteral( "QGroupBox::title { color: %1; }" ).arg( widgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
1723 }
1724 }
1725 if ( widgetInfo.labelStyle.overrideFont )
1726 {
1727 widgetInfo.widget->setFont( widgetInfo.labelStyle.font );
1728 }
1729
1730 layout->addWidget( widgetInfo.widget, row, column, 1, 2 );
1731 if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
1732 {
1733 layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
1734 }
1735 if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
1736 {
1737 layout->setRowStretch( row, widgDef->verticalStretch() );
1738 addSpacer = false;
1739 }
1740
1741 if ( containerDef->visibilityExpression().enabled() || containerDef->collapsedExpression().enabled() )
1742 {
1743 registerContainerInformation( new ContainerInformation( widgetInfo.widget, containerDef->visibilityExpression().enabled() ? containerDef->visibilityExpression().data() : QgsExpression(), containerDef->collapsed(), containerDef->collapsedExpression().enabled() ? containerDef->collapsedExpression().data() : QgsExpression() ) );
1744 }
1745 column += 2;
1746 break;
1747 }
1748
1750 {
1751 tabWidget = nullptr;
1752 WidgetInfo widgetInfo = createWidgetFromDef( widgDef, formWidget, mLayer, mContext );
1753 layout->addWidget( widgetInfo.widget, row, column, 1, 2 );
1754 if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
1755 {
1756 layout->setRowStretch( row, widgDef->verticalStretch() );
1757 addSpacer = false;
1758 }
1759 if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
1760 {
1761 layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
1762 }
1763
1764 if ( containerDef->visibilityExpression().enabled() || containerDef->collapsedExpression().enabled() )
1765 {
1766 registerContainerInformation( new ContainerInformation( widgetInfo.widget, containerDef->visibilityExpression().enabled() ? containerDef->visibilityExpression().data() : QgsExpression(), containerDef->collapsed(), containerDef->collapsedExpression().enabled() ? containerDef->collapsedExpression().data() : QgsExpression() ) );
1767 }
1768 column += 2;
1769 break;
1770 }
1771
1773 {
1774 if ( !tabWidget )
1775 {
1776 tabWidget = new QgsTabWidget();
1777 layout->addWidget( tabWidget, row, column, 1, 2 );
1778 column += 2;
1779 }
1780
1781 QWidget *tabPage = new QWidget( tabWidget );
1782
1783 tabWidget->addTab( tabPage, widgDef->name() );
1784 tabWidget->setTabStyle( tabWidget->tabBar()->count() - 1, widgDef->labelStyle() );
1785
1786 if ( containerDef->visibilityExpression().enabled() )
1787 {
1788 registerContainerInformation( new ContainerInformation( tabWidget, tabPage, containerDef->visibilityExpression().data() ) );
1789 }
1790 QGridLayout *tabPageLayout = new QGridLayout();
1791 tabPage->setLayout( tabPageLayout );
1792
1793 WidgetInfo widgetInfo = createWidgetFromDef( widgDef, tabPage, mLayer, mContext );
1794 tabPageLayout->addWidget( widgetInfo.widget );
1795 break;
1796 }
1797 }
1798 }
1799 else if ( widgDef->type() == Qgis::AttributeEditorType::Relation )
1800 {
1801 hasRootFields = true;
1802 tabWidget = nullptr;
1803 WidgetInfo widgetInfo = createWidgetFromDef( widgDef, container, mLayer, mContext );
1804 QgsCollapsibleGroupBox *collapsibleGroupBox = new QgsCollapsibleGroupBox();
1805
1806 if ( widgetInfo.showLabel )
1807 {
1808 if ( widgetInfo.labelStyle.overrideColor && widgetInfo.labelStyle.color.isValid() )
1809 {
1810 collapsibleGroupBox->setStyleSheet( QStringLiteral( "QGroupBox::title { color: %1; }" ).arg( widgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
1811 }
1812
1813 if ( widgetInfo.labelStyle.overrideFont )
1814 {
1815 collapsibleGroupBox->setFont( widgetInfo.labelStyle.font );
1816 }
1817
1818 collapsibleGroupBox->setTitle( widgetInfo.labelText );
1819 }
1820
1821 QVBoxLayout *collapsibleGroupBoxLayout = new QVBoxLayout();
1822 collapsibleGroupBoxLayout->addWidget( widgetInfo.widget );
1823 collapsibleGroupBox->setLayout( collapsibleGroupBoxLayout );
1824
1825 QVBoxLayout *c = new QVBoxLayout();
1826 c->addWidget( collapsibleGroupBox );
1827 layout->addLayout( c, row, column, 1, 2 );
1828
1829 if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
1830 layout->setRowStretch( row, widgDef->verticalStretch() );
1831 if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
1832 layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
1833
1834 column += 2;
1835
1836 // we consider all relation editors should be expanding
1837 addSpacer = false;
1838 }
1839 else
1840 {
1841 hasRootFields = true;
1842 tabWidget = nullptr;
1843 WidgetInfo widgetInfo = createWidgetFromDef( widgDef, container, mLayer, mContext );
1844 QLabel *label = new QLabel( widgetInfo.labelText );
1845
1846 if ( widgetInfo.labelStyle.overrideColor )
1847 {
1848 if ( widgetInfo.labelStyle.color.isValid() )
1849 {
1850 label->setStyleSheet( QStringLiteral( "QLabel { color: %1; }" ).arg( widgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
1851 }
1852 }
1853
1854 if ( widgetInfo.labelStyle.overrideFont )
1855 {
1856 label->setFont( widgetInfo.labelStyle.font );
1857 }
1858
1859 label->setToolTip( widgetInfo.toolTip );
1860 if ( columnCount > 1 && !widgetInfo.labelOnTop )
1861 {
1862 label->setAlignment( Qt::AlignRight | Qt::AlignVCenter );
1863 }
1864
1865 label->setBuddy( widgetInfo.widget );
1866
1867 // If at least one expanding widget is present do not add a spacer
1868 if ( widgetInfo.widget
1869 && widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Fixed
1870 && widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Maximum
1871 && widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Preferred )
1872 addSpacer = false;
1873
1874 if ( !widgetInfo.showLabel )
1875 {
1876 QVBoxLayout *c = new QVBoxLayout();
1877 label->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Fixed );
1878 c->addWidget( widgetInfo.widget );
1879 layout->addLayout( c, row, column, 1, 2 );
1880
1881 if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
1882 {
1883 layout->setRowStretch( row, widgDef->verticalStretch() );
1884 addSpacer = false;
1885 }
1886 if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
1887 {
1888 layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
1889 }
1890
1891 column += 2;
1892 }
1893 else if ( widgetInfo.labelOnTop )
1894 {
1895 QVBoxLayout *c = new QVBoxLayout();
1896 label->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Fixed );
1897 c->addWidget( label );
1898 c->addWidget( widgetInfo.widget );
1899 layout->addLayout( c, row, column, 1, 2 );
1900
1901 if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
1902 {
1903 layout->setRowStretch( row, widgDef->verticalStretch() );
1904 addSpacer = false;
1905 }
1906 if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
1907 {
1908 layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
1909 }
1910
1911 column += 2;
1912 }
1913 else
1914 {
1915 const int widgetColumn = column + 1;
1916 layout->addWidget( label, row, column++ );
1917 layout->addWidget( widgetInfo.widget, row, column++ );
1918
1919 if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
1920 {
1921 layout->setRowStretch( row, widgDef->verticalStretch() );
1922 addSpacer = false;
1923 }
1924 if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( widgetColumn ) )
1925 {
1926 layout->setColumnStretch( widgetColumn, widgDef->horizontalStretch() );
1927 }
1928 }
1929
1930 // Alias DD overrides
1931 if ( widgDef->type() == Qgis::AttributeEditorType::Field )
1932 {
1933 const QgsAttributeEditorField *fieldElement { static_cast<QgsAttributeEditorField *>( widgDef ) };
1934 const int fieldIdx = fieldElement->idx();
1935 if ( fieldIdx >= 0 && fieldIdx < mLayer->fields().count() )
1936 {
1937 const QString fieldName { mLayer->fields().at( fieldIdx ).name() };
1939 {
1941 if ( property.isActive() )
1942 {
1943 mLabelDataDefinedProperties[ label ] = property;
1944 }
1945 }
1947 {
1949 if ( property.isActive() )
1950 {
1951 mEditableDataDefinedProperties[ widgetInfo.widget ] = property;
1952 }
1953 }
1954 }
1955 }
1956 }
1957
1958 if ( column >= columnCount * 2 )
1959 {
1960 column = 0;
1961 row += 1;
1962 }
1963 }
1964
1965 if ( hasRootFields && addSpacer )
1966 {
1967 QSpacerItem *spacerItem = new QSpacerItem( 20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding );
1968 layout->addItem( spacerItem, row, 0 );
1969 layout->setRowStretch( row, 1 );
1970 }
1971
1972 formWidget = container;
1973 }
1974
1975 // Autogenerate Layout
1976 // If there is still no layout loaded (defined as autogenerate or other methods failed)
1977 mIconMap.clear();
1978
1979 if ( !formWidget )
1980 {
1981 formWidget = new QWidget( this );
1982 QGridLayout *gridLayout = new QGridLayout( formWidget );
1983 formWidget->setLayout( gridLayout );
1984
1985 if ( mContext.formMode() != QgsAttributeEditorContext::Embed )
1986 {
1987 // put the form into a scroll area to nicely handle cases with lots of attributes
1988 QgsScrollArea *scrollArea = new QgsScrollArea( this );
1989 scrollArea->setWidget( formWidget );
1990 scrollArea->setWidgetResizable( true );
1991 scrollArea->setFrameShape( QFrame::NoFrame );
1992 scrollArea->setFrameShadow( QFrame::Plain );
1993 scrollArea->setFocusProxy( this );
1994 layout->addWidget( scrollArea );
1995 }
1996 else
1997 {
1998 layout->addWidget( formWidget );
1999 }
2000
2001 int row = 0;
2002
2003 const QgsFields fields = mLayer->fields();
2004
2005 for ( const QgsField &field : fields )
2006 {
2007 int idx = fields.lookupField( field.name() );
2008 if ( idx < 0 )
2009 continue;
2010
2011 //show attribute alias if available
2012 QString fieldName = mLayer->attributeDisplayName( idx );
2013 QString labelText = fieldName;
2014 labelText.replace( '&', QLatin1String( "&&" ) ); // need to escape '&' or they'll be replace by _ in the label text
2015
2016 const QgsEditorWidgetSetup widgetSetup = QgsGui::editorWidgetRegistry()->findBest( mLayer, field.name() );
2017
2018 if ( widgetSetup.type() == QLatin1String( "Hidden" ) )
2019 continue;
2020
2021 bool labelOnTop = mLayer->editFormConfig().labelOnTop( idx );
2022
2023 // This will also create the widget
2024 QLabel *label = new QLabel( labelText );
2025 label->setToolTip( QgsFieldModel::fieldToolTipExtended( field, mLayer ) );
2026 QSvgWidget *i = new QSvgWidget();
2027 i->setFixedSize( 18, 18 );
2028
2030 {
2032 if ( property.isActive() )
2033 {
2034 mLabelDataDefinedProperties[ label ] = property;
2035 }
2036 }
2037
2038 QgsEditorWidgetWrapper *eww = QgsGui::editorWidgetRegistry()->create( widgetSetup.type(), mLayer, idx, widgetSetup.config(), nullptr, this, mContext );
2039
2040 QWidget *w = nullptr;
2041 if ( eww )
2042 {
2043 QgsAttributeFormEditorWidget *formWidget = new QgsAttributeFormEditorWidget( eww, widgetSetup.type(), this );
2044 w = formWidget;
2045 mFormEditorWidgets.insert( idx, formWidget );
2046 mFormWidgets.append( formWidget );
2047 formWidget->createSearchWidgetWrappers( mContext );
2048
2049 label->setBuddy( eww->widget() );
2050
2052 {
2054 if ( property.isActive() )
2055 {
2056 mEditableDataDefinedProperties[ formWidget ] = property;
2057 }
2058 }
2059 }
2060 else
2061 {
2062 w = new QLabel( QStringLiteral( "<p style=\"color: red; font-style: italic;\">%1</p>" ).arg( tr( "Failed to create widget with type '%1'" ).arg( widgetSetup.type() ) ) );
2063 }
2064
2065
2066 if ( w )
2067 w->setObjectName( field.name() );
2068
2069 if ( eww )
2070 {
2071 mWidgets.append( eww );
2072 mIconMap[eww->widget()] = i;
2073 }
2074
2075 if ( labelOnTop )
2076 {
2077 gridLayout->addWidget( label, row++, 0, 1, 2 );
2078 gridLayout->addWidget( w, row++, 0, 1, 2 );
2079 gridLayout->addWidget( i, row++, 0, 1, 2 );
2080 }
2081 else
2082 {
2083 gridLayout->addWidget( label, row, 0 );
2084 gridLayout->addWidget( w, row, 1 );
2085 gridLayout->addWidget( i, row++, 2 );
2086 }
2087
2088 }
2089
2090 const QList<QgsRelation> relations = QgsProject::instance()->relationManager()->referencedRelations( mLayer );
2091 for ( const QgsRelation &rel : relations )
2092 {
2093 QgsRelationWidgetWrapper *rww = setupRelationWidgetWrapper( QStringLiteral( "relation_editor" ), rel, mContext );
2094
2096 formWidget->createSearchWidgetWrappers( mContext );
2097
2098 QgsCollapsibleGroupBox *collapsibleGroupBox = new QgsCollapsibleGroupBox( rel.name() );
2099 QVBoxLayout *collapsibleGroupBoxLayout = new QVBoxLayout();
2100 collapsibleGroupBoxLayout->addWidget( formWidget );
2101 collapsibleGroupBox->setLayout( collapsibleGroupBoxLayout );
2102
2103 gridLayout->addWidget( collapsibleGroupBox, row++, 0, 1, 2 );
2104
2105 mWidgets.append( rww );
2106 mFormWidgets.append( formWidget );
2107 }
2108
2109 if ( QgsProject::instance()->relationManager()->referencedRelations( mLayer ).isEmpty() )
2110 {
2111 QSpacerItem *spacerItem = new QSpacerItem( 20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding );
2112 gridLayout->addItem( spacerItem, row, 0 );
2113 gridLayout->setRowStretch( row, 1 );
2114 row++;
2115 }
2116 }
2117
2118 // Prepare value dependencies
2119 updateFieldDependencies();
2120
2121 if ( !mButtonBox )
2122 {
2123 mButtonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel );
2124 mButtonBox->setObjectName( QStringLiteral( "buttonBox" ) );
2125 layout->addWidget( mButtonBox, layout->rowCount(), 0, 1, layout->columnCount() );
2126 }
2127 mButtonBox->setVisible( buttonBoxVisible );
2128
2129 if ( !mSearchButtonBox )
2130 {
2131 mSearchButtonBox = new QWidget();
2132 QHBoxLayout *boxLayout = new QHBoxLayout();
2133 boxLayout->setContentsMargins( 0, 0, 0, 0 );
2134 mSearchButtonBox->setLayout( boxLayout );
2135 mSearchButtonBox->setObjectName( QStringLiteral( "searchButtonBox" ) );
2136
2137 QPushButton *clearButton = new QPushButton( tr( "&Reset Form" ), mSearchButtonBox );
2138 connect( clearButton, &QPushButton::clicked, this, &QgsAttributeForm::resetSearch );
2139 boxLayout->addWidget( clearButton );
2140 boxLayout->addStretch( 1 );
2141
2142 QPushButton *flashButton = new QPushButton();
2143 flashButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
2144 flashButton->setText( tr( "&Flash Features" ) );
2145 connect( flashButton, &QToolButton::clicked, this, &QgsAttributeForm::searchFlash );
2146 boxLayout->addWidget( flashButton );
2147
2148 QPushButton *openAttributeTableButton = new QPushButton();
2149 openAttributeTableButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
2150 openAttributeTableButton->setText( tr( "Show in &Table" ) );
2151 openAttributeTableButton->setToolTip( tr( "Open the attribute table editor with the filtered features" ) );
2152 connect( openAttributeTableButton, &QToolButton::clicked, this, [ = ]
2153 {
2154 emit openFilteredFeaturesAttributeTable( createFilterExpression() );
2155 } );
2156 boxLayout->addWidget( openAttributeTableButton );
2157
2158 QPushButton *zoomButton = new QPushButton();
2159 zoomButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
2160 zoomButton->setText( tr( "&Zoom to Features" ) );
2161 connect( zoomButton, &QToolButton::clicked, this, &QgsAttributeForm::searchZoomTo );
2162 boxLayout->addWidget( zoomButton );
2163
2164 QToolButton *selectButton = new QToolButton();
2165 selectButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
2166 selectButton->setText( tr( "&Select Features" ) );
2167 selectButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconFormSelect.svg" ) ) );
2168 selectButton->setPopupMode( QToolButton::MenuButtonPopup );
2169 selectButton->setToolButtonStyle( Qt::ToolButtonTextBesideIcon );
2170 connect( selectButton, &QToolButton::clicked, this, &QgsAttributeForm::searchSetSelection );
2171 QMenu *selectMenu = new QMenu( selectButton );
2172 QAction *selectAction = new QAction( tr( "Select Features" ), selectMenu );
2173 selectAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconFormSelect.svg" ) ) );
2174 connect( selectAction, &QAction::triggered, this, &QgsAttributeForm::searchSetSelection );
2175 selectMenu->addAction( selectAction );
2176 QAction *addSelectAction = new QAction( tr( "Add to Current Selection" ), selectMenu );
2177 addSelectAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconSelectAdd.svg" ) ) );
2178 connect( addSelectAction, &QAction::triggered, this, &QgsAttributeForm::searchAddToSelection );
2179 selectMenu->addAction( addSelectAction );
2180 QAction *deselectAction = new QAction( tr( "Remove from Current Selection" ), selectMenu );
2181 deselectAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconSelectRemove.svg" ) ) );
2182 connect( deselectAction, &QAction::triggered, this, &QgsAttributeForm::searchRemoveFromSelection );
2183 selectMenu->addAction( deselectAction );
2184 QAction *filterSelectAction = new QAction( tr( "Filter Current Selection" ), selectMenu );
2185 filterSelectAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconSelectIntersect.svg" ) ) );
2186 connect( filterSelectAction, &QAction::triggered, this, &QgsAttributeForm::searchIntersectSelection );
2187 selectMenu->addAction( filterSelectAction );
2188 selectButton->setMenu( selectMenu );
2189 boxLayout->addWidget( selectButton );
2190
2191 if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
2192 {
2193 QToolButton *filterButton = new QToolButton();
2194 filterButton->setText( tr( "Filter Features" ) );
2195 filterButton->setPopupMode( QToolButton::MenuButtonPopup );
2196 filterButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
2197 connect( filterButton, &QToolButton::clicked, this, &QgsAttributeForm::filterTriggered );
2198 QMenu *filterMenu = new QMenu( filterButton );
2199 QAction *filterAndAction = new QAction( tr( "Filter Within (\"AND\")" ), filterMenu );
2200 connect( filterAndAction, &QAction::triggered, this, &QgsAttributeForm::filterAndTriggered );
2201 filterMenu->addAction( filterAndAction );
2202 QAction *filterOrAction = new QAction( tr( "Extend Filter (\"OR\")" ), filterMenu );
2203 connect( filterOrAction, &QAction::triggered, this, &QgsAttributeForm::filterOrTriggered );
2204 filterMenu->addAction( filterOrAction );
2205 filterButton->setMenu( filterMenu );
2206 boxLayout->addWidget( filterButton );
2207 }
2208 else
2209 {
2210 QPushButton *closeButton = new QPushButton( tr( "Close" ), mSearchButtonBox );
2211 connect( closeButton, &QPushButton::clicked, this, &QgsAttributeForm::closed );
2212 closeButton->setShortcut( Qt::Key_Escape );
2213 boxLayout->addWidget( closeButton );
2214 }
2215
2216 layout->addWidget( mSearchButtonBox, layout->rowCount(), 0, 1, layout->columnCount() );
2217 }
2218 mSearchButtonBox->setVisible( mMode == QgsAttributeEditorContext::SearchMode );
2219
2220 afterWidgetInit();
2221
2222 connect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsAttributeForm::save );
2223 connect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsAttributeForm::resetValues );
2224
2225 connect( mLayer, &QgsVectorLayer::editingStarted, this, &QgsAttributeForm::synchronizeState );
2226 connect( mLayer, &QgsVectorLayer::editingStopped, this, &QgsAttributeForm::synchronizeState );
2227
2228 // This triggers a refresh of the form widget and gives a chance to re-format the
2229 // value to those widgets that have a different representation when in edit mode
2232
2233
2234 const auto constMInterfaces = mInterfaces;
2235 for ( QgsAttributeFormInterface *iface : constMInterfaces )
2236 {
2237 iface->initForm();
2238 }
2239
2241 {
2242 hideButtonBox();
2243 }
2244
2245 QApplication::restoreOverrideCursor();
2246}
2247
2248void QgsAttributeForm::cleanPython()
2249{
2250 if ( !mPyFormVarName.isNull() )
2251 {
2252 QString expr = QStringLiteral( "if '%1' in locals(): del %1\n" ).arg( mPyFormVarName );
2253 QgsPythonRunner::run( expr );
2254 }
2255}
2256
2257void QgsAttributeForm::initPython()
2258{
2259 cleanPython();
2260
2261 // Init Python, if init function is not empty and the combo indicates
2262 // the source for the function code
2263 if ( !mLayer->editFormConfig().initFunction().isEmpty()
2265 {
2266
2267 QString initFunction = mLayer->editFormConfig().initFunction();
2268 QString initFilePath = mLayer->editFormConfig().initFilePath();
2269 QString initCode;
2270
2271 switch ( mLayer->editFormConfig().initCodeSource() )
2272 {
2274 if ( !initFilePath.isEmpty() )
2275 {
2276 QFile *inputFile = QgsApplication::networkContentFetcherRegistry()->localFile( initFilePath );
2277
2278 if ( inputFile && inputFile->open( QFile::ReadOnly ) )
2279 {
2280 // Read it into a string
2281 QTextStream inf( inputFile );
2282#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
2283 inf.setCodec( "UTF-8" );
2284#endif
2285 initCode = inf.readAll();
2286 inputFile->close();
2287 }
2288 else // The file couldn't be opened
2289 {
2290 QgsLogger::warning( QStringLiteral( "The external python file path %1 could not be opened!" ).arg( initFilePath ) );
2291 }
2292 }
2293 else
2294 {
2295 QgsLogger::warning( QStringLiteral( "The external python file path is empty!" ) );
2296 }
2297 break;
2298
2300 initCode = mLayer->editFormConfig().initCode();
2301 if ( initCode.isEmpty() )
2302 {
2303 QgsLogger::warning( QStringLiteral( "The python code provided in the dialog is empty!" ) );
2304 }
2305 break;
2306
2309 // Nothing to do: the function code should be already in the environment
2310 break;
2311 }
2312
2313 // If we have a function code, run it
2314 if ( !initCode.isEmpty() )
2315 {
2317 QgsPythonRunner::run( initCode );
2318 else
2319 mMessageBar->pushMessage( QString(),
2320 tr( "Python macro could not be run due to missing permissions." ),
2321 Qgis::MessageLevel::Warning );
2322 }
2323
2324 QgsPythonRunner::run( QStringLiteral( "import inspect" ) );
2325 QString numArgs;
2326
2327 // Check for eval result
2328 if ( QgsPythonRunner::eval( QStringLiteral( "len(inspect.getfullargspec(%1)[0])" ).arg( initFunction ), numArgs ) )
2329 {
2330 static int sFormId = 0;
2331 mPyFormVarName = QStringLiteral( "_qgis_featureform_%1_%2" ).arg( mFormNr ).arg( sFormId++ );
2332
2333 QString form = QStringLiteral( "%1 = sip.wrapinstance( %2, qgis.gui.QgsAttributeForm )" )
2334 .arg( mPyFormVarName )
2335 .arg( ( quint64 ) this );
2336
2337 QgsPythonRunner::run( form );
2338
2339 QgsDebugMsgLevel( QStringLiteral( "running featureForm init: %1" ).arg( mPyFormVarName ), 2 );
2340
2341 // Legacy
2342 if ( numArgs == QLatin1String( "3" ) )
2343 {
2344 addInterface( new QgsAttributeFormLegacyInterface( initFunction, mPyFormVarName, this ) );
2345 }
2346 else
2347 {
2348 // If we get here, it means that the function doesn't accept three arguments
2349 QMessageBox msgBox;
2350 msgBox.setText( 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 ) );
2351 msgBox.exec();
2352#if 0
2353 QString expr = QString( "%1(%2)" )
2354 .arg( mLayer->editFormInit() )
2355 .arg( mPyFormVarName );
2356 QgsAttributeFormInterface *iface = QgsPythonRunner::evalToSipObject<QgsAttributeFormInterface *>( expr, "QgsAttributeFormInterface" );
2357 if ( iface )
2358 addInterface( iface );
2359#endif
2360 }
2361 }
2362 else
2363 {
2364 // If we get here, it means that inspect couldn't find the function
2365 QMessageBox msgBox;
2366 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 ) );
2367 msgBox.exec();
2368 }
2369 }
2370}
2371
2372QgsAttributeForm::WidgetInfo QgsAttributeForm::createWidgetFromDef( const QgsAttributeEditorElement *widgetDef, QWidget *parent, QgsVectorLayer *vl, QgsAttributeEditorContext &context )
2373{
2374 WidgetInfo newWidgetInfo;
2375
2376 newWidgetInfo.labelStyle = widgetDef->labelStyle();
2377
2378 switch ( widgetDef->type() )
2379 {
2381 {
2382 const QgsAttributeEditorAction *elementDef = dynamic_cast<const QgsAttributeEditorAction *>( widgetDef );
2383 if ( !elementDef )
2384 break;
2385
2386 QgsActionWidgetWrapper *actionWrapper = new QgsActionWidgetWrapper( mLayer, nullptr, this );
2387 actionWrapper->setAction( elementDef->action( vl ) );
2388 context.setAttributeFormMode( mMode );
2389 actionWrapper->setContext( context );
2390 mWidgets.append( actionWrapper );
2391 newWidgetInfo.widget = actionWrapper->widget();
2392 newWidgetInfo.showLabel = false;
2393
2394 break;
2395 }
2396
2398 {
2399 const QgsAttributeEditorField *fieldDef = dynamic_cast<const QgsAttributeEditorField *>( widgetDef );
2400 if ( !fieldDef )
2401 break;
2402
2403 const QgsFields fields = vl->fields();
2404 int fldIdx = fields.lookupField( fieldDef->name() );
2405 if ( fldIdx < fields.count() && fldIdx >= 0 )
2406 {
2407 const QgsEditorWidgetSetup widgetSetup = QgsGui::editorWidgetRegistry()->findBest( mLayer, fieldDef->name() );
2408
2409 QgsEditorWidgetWrapper *eww = QgsGui::editorWidgetRegistry()->create( widgetSetup.type(), mLayer, fldIdx, widgetSetup.config(), nullptr, this, mContext );
2410 QgsAttributeFormEditorWidget *formWidget = new QgsAttributeFormEditorWidget( eww, widgetSetup.type(), this );
2411 mFormEditorWidgets.insert( fldIdx, formWidget );
2412 mFormWidgets.append( formWidget );
2413
2414 formWidget->createSearchWidgetWrappers( mContext );
2415
2416 newWidgetInfo.widget = formWidget;
2417 mWidgets.append( eww );
2418
2419 newWidgetInfo.widget->setObjectName( fields.at( fldIdx ).name() );
2420 newWidgetInfo.hint = fields.at( fldIdx ).comment();
2421 }
2422
2423 newWidgetInfo.labelOnTop = mLayer->editFormConfig().labelOnTop( fldIdx );
2424 newWidgetInfo.labelText = mLayer->attributeDisplayName( fldIdx );
2425 newWidgetInfo.labelText.replace( '&', QLatin1String( "&&" ) ); // need to escape '&' or they'll be replace by _ in the label text
2426 newWidgetInfo.toolTip = QStringLiteral( "<b>%1</b><p>%2</p>" ).arg( mLayer->attributeDisplayName( fldIdx ), newWidgetInfo.hint );
2427 newWidgetInfo.showLabel = widgetDef->showLabel();
2428
2429 break;
2430 }
2431
2433 {
2434 const QgsAttributeEditorRelation *relDef = static_cast<const QgsAttributeEditorRelation *>( widgetDef );
2435
2436 QgsRelationWidgetWrapper *rww = setupRelationWidgetWrapper( relDef->relationWidgetTypeId(), relDef->relation(), context );
2437
2439 formWidget->createSearchWidgetWrappers( mContext );
2440
2441 // This needs to be after QgsAttributeFormRelationEditorWidget creation, because the widget
2442 // does not exists yet until QgsAttributeFormRelationEditorWidget is created and the setters
2443 // below directly alter the widget and check for it.
2445 rww->setNmRelationId( relDef->nmRelationId() );
2447
2448 mWidgets.append( rww );
2449 mFormWidgets.append( formWidget );
2450
2451 newWidgetInfo.widget = formWidget;
2452 newWidgetInfo.showLabel = relDef->showLabel();
2453 newWidgetInfo.labelText = relDef->label();
2454 if ( newWidgetInfo.labelText.isEmpty() )
2455 newWidgetInfo.labelText = rww->relation().name();
2456 newWidgetInfo.labelOnTop = true;
2457 break;
2458 }
2459
2461 {
2462 const QgsAttributeEditorContainer *container = dynamic_cast<const QgsAttributeEditorContainer *>( widgetDef );
2463 if ( !container )
2464 break;
2465
2466 int columnCount = container->columnCount();
2467
2468 if ( columnCount <= 0 )
2469 columnCount = 1;
2470
2471 QString widgetName;
2472 QWidget *myContainer = nullptr;
2473 bool removeLayoutMargin = false;
2474 switch ( container->type() )
2475 {
2477 {
2479 widgetName = QStringLiteral( "QGroupBox" );
2480 if ( container->showLabel() )
2481 {
2482 groupBox->setTitle( container->name() );
2483 if ( newWidgetInfo.labelStyle.overrideColor )
2484 {
2485 if ( newWidgetInfo.labelStyle.color.isValid() )
2486 {
2487 groupBox->setStyleSheet( QStringLiteral( "QGroupBox::title { color: %1; }" ).arg( newWidgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
2488 }
2489 }
2490 if ( newWidgetInfo.labelStyle.overrideFont )
2491 {
2492 groupBox->setFont( newWidgetInfo.labelStyle.font );
2493 }
2494 }
2495 myContainer = groupBox;
2496 newWidgetInfo.widget = myContainer;
2497 groupBox->setCollapsed( container->collapsed() );
2498 break;
2499 }
2500
2502 {
2503 QWidget *rowWidget = new QWidget();
2504 widgetName = QStringLiteral( "Row" );
2505 myContainer = rowWidget;
2506 newWidgetInfo.widget = myContainer;
2507 removeLayoutMargin = true;
2508 columnCount = container->children().size();
2509 break;
2510 }
2511
2513 {
2514 myContainer = new QWidget();
2515
2516 QgsScrollArea *scrollArea = new QgsScrollArea( parent );
2517
2518 scrollArea->setWidget( myContainer );
2519 scrollArea->setWidgetResizable( true );
2520 scrollArea->setFrameShape( QFrame::NoFrame );
2521 widgetName = QStringLiteral( "QScrollArea QWidget" );
2522
2523 newWidgetInfo.widget = scrollArea;
2524 break;
2525 }
2526 }
2527
2528 if ( container->backgroundColor().isValid() )
2529 {
2530 QString style {QStringLiteral( "background-color: %1;" ).arg( container->backgroundColor().name() )};
2531 newWidgetInfo.widget->setStyleSheet( style );
2532 }
2533
2534 QGridLayout *gbLayout = new QGridLayout();
2535 if ( removeLayoutMargin )
2536 gbLayout->setContentsMargins( 0, 0, 0, 0 );
2537 myContainer->setLayout( gbLayout );
2538
2539 int row = 0;
2540 int column = 0;
2541 bool addSpacer = true;
2542
2543 const QList<QgsAttributeEditorElement *> children = container->children();
2544
2545 for ( QgsAttributeEditorElement *childDef : children )
2546 {
2547 WidgetInfo widgetInfo = createWidgetFromDef( childDef, myContainer, vl, context );
2548
2549 if ( childDef->type() == Qgis::AttributeEditorType::Container )
2550 {
2551 QgsAttributeEditorContainer *containerDef = static_cast<QgsAttributeEditorContainer *>( childDef );
2552 if ( containerDef->visibilityExpression().enabled() || containerDef->collapsedExpression().enabled() )
2553 {
2554 registerContainerInformation( new ContainerInformation( widgetInfo.widget, containerDef->visibilityExpression().enabled() ? containerDef->visibilityExpression().data() : QgsExpression(), containerDef->collapsed(), containerDef->collapsedExpression().enabled() ? containerDef->collapsedExpression().data() : QgsExpression() ) );
2555 }
2556 }
2557
2558 // column containing the actual widget, not the label
2559 int widgetColumn = column;
2560
2561 if ( widgetInfo.labelText.isNull() || ! widgetInfo.showLabel )
2562 {
2563 gbLayout->addWidget( widgetInfo.widget, row, column, 1, 2 );
2564 widgetColumn = column + 1;
2565 column += 2;
2566 }
2567 else
2568 {
2569 QLabel *mypLabel = new QLabel( widgetInfo.labelText );
2570
2571 if ( widgetInfo.labelStyle.overrideColor )
2572 {
2573 if ( widgetInfo.labelStyle.color.isValid() )
2574 {
2575 mypLabel->setStyleSheet( QStringLiteral( "QLabel { color: %1; }" ).arg( widgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
2576 }
2577 }
2578
2579 if ( widgetInfo.labelStyle.overrideFont )
2580 {
2581 mypLabel->setFont( widgetInfo.labelStyle.font );
2582 }
2583
2584 // Alias DD overrides
2585 if ( childDef->type() == Qgis::AttributeEditorType::Field )
2586 {
2587 const QgsAttributeEditorField *fieldDef { static_cast<QgsAttributeEditorField *>( childDef ) };
2588 const QgsFields fields = vl->fields();
2589 const int fldIdx = fieldDef->idx();
2590 if ( fldIdx < fields.count() && fldIdx >= 0 )
2591 {
2592 const QString fieldName { fields.at( fldIdx ).name() };
2594 {
2596 if ( property.isActive() )
2597 {
2598 mLabelDataDefinedProperties[ mypLabel ] = property;
2599 }
2600 }
2602 {
2604 if ( property.isActive() )
2605 {
2606 mEditableDataDefinedProperties[ widgetInfo.widget ] = property;
2607 }
2608 }
2609 }
2610 }
2611
2612 mypLabel->setToolTip( widgetInfo.toolTip );
2613 if ( columnCount > 1 && !widgetInfo.labelOnTop )
2614 {
2615 mypLabel->setAlignment( Qt::AlignRight | Qt::AlignVCenter );
2616 }
2617
2618 mypLabel->setBuddy( widgetInfo.widget );
2619
2620 if ( widgetInfo.labelOnTop )
2621 {
2622 widgetColumn = column + 1;
2623 QVBoxLayout *c = new QVBoxLayout();
2624 mypLabel->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Fixed );
2625 c->layout()->addWidget( mypLabel );
2626 c->layout()->addWidget( widgetInfo.widget );
2627 gbLayout->addLayout( c, row, column, 1, 2 );
2628 column += 2;
2629 }
2630 else
2631 {
2632 widgetColumn = column + 1;
2633 gbLayout->addWidget( mypLabel, row, column++ );
2634 gbLayout->addWidget( widgetInfo.widget, row, column++ );
2635 }
2636 }
2637
2638 const int childHorizontalStretch = childDef->horizontalStretch();
2639 const int existingColumnStretch = gbLayout->columnStretch( widgetColumn );
2640 if ( childHorizontalStretch > 0 && childHorizontalStretch > existingColumnStretch )
2641 {
2642 gbLayout->setColumnStretch( widgetColumn, childHorizontalStretch );
2643 }
2644
2645 if ( childDef->verticalStretch() > 0 && childDef->verticalStretch() > gbLayout->rowStretch( row ) )
2646 {
2647 gbLayout->setRowStretch( row, childDef->verticalStretch() );
2648 }
2649
2650 if ( column >= columnCount * 2 )
2651 {
2652 column = 0;
2653 row += 1;
2654 }
2655
2656 if ( widgetInfo.widget
2657 && widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Fixed
2658 && widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Maximum
2659 && widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Preferred )
2660 addSpacer = false;
2661
2662 // we consider all relation editors should be expanding
2663 if ( qobject_cast<QgsAttributeFormRelationEditorWidget *>( widgetInfo.widget ) )
2664 addSpacer = false;
2665 }
2666
2667 if ( addSpacer )
2668 {
2669 QWidget *spacer = new QWidget();
2670 spacer->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Preferred );
2671 gbLayout->addWidget( spacer, ++row, 0 );
2672 gbLayout->setRowStretch( row, 1 );
2673 }
2674
2675 newWidgetInfo.labelText = QString();
2676 newWidgetInfo.labelOnTop = true;
2677 newWidgetInfo.showLabel = widgetDef->showLabel();
2678 break;
2679 }
2680
2682 {
2683 const QgsAttributeEditorQmlElement *elementDef = static_cast<const QgsAttributeEditorQmlElement *>( widgetDef );
2684
2685 QgsQmlWidgetWrapper *qmlWrapper = new QgsQmlWidgetWrapper( mLayer, nullptr, this );
2686 qmlWrapper->setQmlCode( elementDef->qmlCode() );
2687 context.setAttributeFormMode( mMode );
2688 qmlWrapper->setContext( context );
2689
2690 mWidgets.append( qmlWrapper );
2691
2692 newWidgetInfo.widget = qmlWrapper->widget();
2693 newWidgetInfo.labelText = elementDef->name();
2694 newWidgetInfo.labelOnTop = true;
2695 newWidgetInfo.showLabel = widgetDef->showLabel();
2696 break;
2697 }
2698
2700 {
2701 const QgsAttributeEditorHtmlElement *elementDef = static_cast<const QgsAttributeEditorHtmlElement *>( widgetDef );
2702
2703 QgsHtmlWidgetWrapper *htmlWrapper = new QgsHtmlWidgetWrapper( mLayer, nullptr, this );
2704 context.setAttributeFormMode( mMode );
2705 htmlWrapper->setHtmlCode( elementDef->htmlCode() );
2706 htmlWrapper->reinitWidget();
2707 mWidgets.append( htmlWrapper );
2708
2709 newWidgetInfo.widget = htmlWrapper->widget();
2710 newWidgetInfo.labelText = elementDef->name();
2711 newWidgetInfo.labelOnTop = true;
2712 newWidgetInfo.showLabel = widgetDef->showLabel();
2713 mNeedsGeometry |= htmlWrapper->needsGeometry();
2714 break;
2715 }
2716
2718 {
2719 const QgsAttributeEditorTextElement *elementDef = static_cast<const QgsAttributeEditorTextElement *>( widgetDef );
2720
2721 QgsTextWidgetWrapper *textWrapper = new QgsTextWidgetWrapper( mLayer, nullptr, this );
2722 context.setAttributeFormMode( mMode );
2723 textWrapper->setText( elementDef->text() );
2724 textWrapper->reinitWidget();
2725 mWidgets.append( textWrapper );
2726
2727 newWidgetInfo.widget = textWrapper->widget();
2728 newWidgetInfo.labelText = elementDef->name();
2729 newWidgetInfo.labelOnTop = false;
2730 newWidgetInfo.showLabel = widgetDef->showLabel();
2731 mNeedsGeometry |= textWrapper->needsGeometry();
2732 break;
2733 }
2734
2736 {
2737 const QgsAttributeEditorSpacerElement *elementDef = static_cast<const QgsAttributeEditorSpacerElement *>( widgetDef );
2738 QgsSpacerWidgetWrapper *spacerWrapper = new QgsSpacerWidgetWrapper( mLayer, nullptr, this );
2739 spacerWrapper->setDrawLine( elementDef->drawLine() );
2740 context.setAttributeFormMode( mMode );
2741 mWidgets.append( spacerWrapper );
2742
2743 newWidgetInfo.widget = spacerWrapper->widget();
2744 newWidgetInfo.labelOnTop = false;
2745 newWidgetInfo.showLabel = false;
2746 break;
2747 }
2748
2749 default:
2750 QgsDebugError( QStringLiteral( "Unknown attribute editor widget type encountered..." ) );
2751 break;
2752 }
2753
2754 return newWidgetInfo;
2755}
2756
2757void QgsAttributeForm::createWrappers()
2758{
2759 QList<QWidget *> myWidgets = findChildren<QWidget *>();
2760 const QList<QgsField> fields = mLayer->fields().toList();
2761
2762 const auto constMyWidgets = myWidgets;
2763 for ( QWidget *myWidget : constMyWidgets )
2764 {
2765 // Check the widget's properties for a relation definition
2766 QVariant vRel = myWidget->property( "qgisRelation" );
2767 if ( vRel.isValid() )
2768 {
2770 QgsRelation relation = relMgr->relation( vRel.toString() );
2771 if ( relation.isValid() )
2772 {
2773 QgsRelationWidgetWrapper *rww = new QgsRelationWidgetWrapper( mLayer, relation, myWidget, this );
2774 rww->setConfig( mLayer->editFormConfig().widgetConfig( relation.id() ) );
2775 rww->setContext( mContext );
2776 rww->widget(); // Will initialize the widget
2777 mWidgets.append( rww );
2778 }
2779 }
2780 else
2781 {
2782 const auto constFields = fields;
2783 for ( const QgsField &field : constFields )
2784 {
2785 if ( field.name() == myWidget->objectName() )
2786 {
2787 int idx = mLayer->fields().lookupField( field.name() );
2788
2789 QgsEditorWidgetWrapper *eww = QgsGui::editorWidgetRegistry()->create( mLayer, idx, myWidget, this, mContext );
2790 mWidgets.append( eww );
2791 }
2792 }
2793 }
2794 }
2795}
2796
2797void QgsAttributeForm::afterWidgetInit()
2798{
2799 bool isFirstEww = true;
2800
2801 const auto constMWidgets = mWidgets;
2802 for ( QgsWidgetWrapper *ww : constMWidgets )
2803 {
2804 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
2805
2806 if ( eww )
2807 {
2808 if ( isFirstEww )
2809 {
2810 setFocusProxy( eww->widget() );
2811 isFirstEww = false;
2812 }
2813
2814 connect( eww, &QgsEditorWidgetWrapper::valuesChanged, this, &QgsAttributeForm::onAttributeChanged, Qt::UniqueConnection );
2815 connect( eww, &QgsEditorWidgetWrapper::constraintStatusChanged, this, &QgsAttributeForm::onConstraintStatusChanged, Qt::UniqueConnection );
2816 }
2817 else
2818 {
2819 QgsRelationWidgetWrapper *relationWidgetWrapper = qobject_cast<QgsRelationWidgetWrapper *>( ww );
2820 if ( relationWidgetWrapper )
2821 {
2822 connect( relationWidgetWrapper, &QgsRelationWidgetWrapper::relatedFeaturesChanged, this, &QgsAttributeForm::onRelatedFeaturesChanged, static_cast<Qt::ConnectionType>( Qt::UniqueConnection | Qt::QueuedConnection ) );
2823 }
2824 }
2825 }
2826}
2827
2828
2829bool QgsAttributeForm::eventFilter( QObject *object, QEvent *e )
2830{
2831 Q_UNUSED( object )
2832
2833 if ( e->type() == QEvent::KeyPress )
2834 {
2835 QKeyEvent *keyEvent = dynamic_cast<QKeyEvent *>( e );
2836 if ( keyEvent && keyEvent->key() == Qt::Key_Escape )
2837 {
2838 // Re-emit to this form so it will be forwarded to parent
2839 event( e );
2840 return true;
2841 }
2842 }
2843
2844 return false;
2845}
2846
2847void QgsAttributeForm::scanForEqualAttributes( QgsFeatureIterator &fit,
2848 QSet< int > &mixedValueFields,
2849 QHash< int, QVariant > &fieldSharedValues ) const
2850{
2851 mixedValueFields.clear();
2852 fieldSharedValues.clear();
2853
2854 QgsFeature f;
2855 bool first = true;
2856 while ( fit.nextFeature( f ) )
2857 {
2858 for ( int i = 0; i < mLayer->fields().count(); ++i )
2859 {
2860 if ( mixedValueFields.contains( i ) )
2861 continue;
2862
2863 if ( first )
2864 {
2865 fieldSharedValues[i] = f.attribute( i );
2866 }
2867 else
2868 {
2869 if ( fieldSharedValues.value( i ) != f.attribute( i ) )
2870 {
2871 fieldSharedValues.remove( i );
2872 mixedValueFields.insert( i );
2873 }
2874 }
2875 }
2876 first = false;
2877
2878 if ( mixedValueFields.count() == mLayer->fields().count() )
2879 {
2880 // all attributes are mixed, no need to keep scanning
2881 break;
2882 }
2883 }
2884}
2885
2886
2887void QgsAttributeForm::layerSelectionChanged()
2888{
2889 switch ( mMode )
2890 {
2897 break;
2898
2900 resetMultiEdit( true );
2901 break;
2902 }
2903}
2904
2906{
2907 mIsSettingMultiEditFeatures = true;
2908 mMultiEditFeatureIds = fids;
2909
2910 if ( fids.isEmpty() )
2911 {
2912 // no selected features
2913 QMultiMap< int, QgsAttributeFormEditorWidget * >::const_iterator wIt = mFormEditorWidgets.constBegin();
2914 for ( ; wIt != mFormEditorWidgets.constEnd(); ++ wIt )
2915 {
2916 wIt.value()->initialize( QVariant() );
2917 }
2918 mIsSettingMultiEditFeatures = false;
2919 return;
2920 }
2921
2922 QgsFeatureIterator fit = mLayer->getFeatures( QgsFeatureRequest().setFilterFids( fids ) );
2923
2924 // Scan through all features to determine which attributes are initially the same
2925 QSet< int > mixedValueFields;
2926 QHash< int, QVariant > fieldSharedValues;
2927 scanForEqualAttributes( fit, mixedValueFields, fieldSharedValues );
2928
2929 // also fetch just first feature
2930 fit = mLayer->getFeatures( QgsFeatureRequest().setFilterFid( *fids.constBegin() ) );
2931 QgsFeature firstFeature;
2932 fit.nextFeature( firstFeature );
2933
2934 // Make this feature the current form feature or the constraints will be evaluated
2935 // on a possibly wrong previously selected/current feature
2936 if ( mCurrentFormFeature.id() != firstFeature.id( ) )
2937 {
2938 setFeature( firstFeature );
2939 }
2940
2941 const auto constMixedValueFields = mixedValueFields;
2942 for ( int fieldIndex : std::as_const( mixedValueFields ) )
2943 {
2944 const QList<QgsAttributeFormEditorWidget *> formEditorWidgets = mFormEditorWidgets.values( fieldIndex );
2945 if ( formEditorWidgets.isEmpty() )
2946 continue;
2947
2948 const QStringList additionalFields = formEditorWidgets.first()->editorWidget()->additionalFields();
2949 QVariantList additionalFieldValues;
2950 for ( const QString &additionalField : additionalFields )
2951 additionalFieldValues << firstFeature.attribute( additionalField );
2952
2953 for ( QgsAttributeFormEditorWidget *w : formEditorWidgets )
2954 w->initialize( firstFeature.attribute( fieldIndex ), true, additionalFieldValues );
2955 }
2956 QHash< int, QVariant >::const_iterator sharedValueIt = fieldSharedValues.constBegin();
2957 for ( ; sharedValueIt != fieldSharedValues.constEnd(); ++sharedValueIt )
2958 {
2959 const QList<QgsAttributeFormEditorWidget *> formEditorWidgets = mFormEditorWidgets.values( sharedValueIt.key() );
2960 if ( formEditorWidgets.isEmpty() )
2961 continue;
2962
2963 bool mixed = false;
2964 const QStringList additionalFields = formEditorWidgets.first()->editorWidget()->additionalFields();
2965 for ( const QString &additionalField : additionalFields )
2966 {
2967 int index = mLayer->fields().indexFromName( additionalField );
2968 if ( constMixedValueFields.contains( index ) )
2969 {
2970 // if additional field are mixed, it is considered as mixed
2971 mixed = true;
2972 break;
2973 }
2974 }
2975 QVariantList additionalFieldValues;
2976 if ( mixed )
2977 {
2978 for ( const QString &additionalField : additionalFields )
2979 additionalFieldValues << firstFeature.attribute( additionalField );
2980 for ( QgsAttributeFormEditorWidget *w : formEditorWidgets )
2981 w->initialize( firstFeature.attribute( sharedValueIt.key() ), true, additionalFieldValues );
2982 }
2983 else
2984 {
2985 for ( const QString &additionalField : additionalFields )
2986 {
2987 int index = mLayer->fields().indexFromName( additionalField );
2988 Q_ASSERT( fieldSharedValues.contains( index ) );
2989 additionalFieldValues << fieldSharedValues.value( index );
2990 }
2991 for ( QgsAttributeFormEditorWidget *w : formEditorWidgets )
2992 w->initialize( sharedValueIt.value(), false, additionalFieldValues );
2993 }
2994 }
2995
2996 setMultiEditFeatureIdsRelations( fids );
2997
2998 mIsSettingMultiEditFeatures = false;
2999}
3000
3002{
3003 if ( mOwnsMessageBar )
3004 delete mMessageBar;
3005 mOwnsMessageBar = false;
3006 mMessageBar = messageBar;
3007}
3008
3010{
3012 {
3013 Q_ASSERT( false );
3014 }
3015
3016 QStringList filters;
3017 for ( QgsAttributeFormWidget *widget : mFormWidgets )
3018 {
3019 QString filter = widget->currentFilterExpression();
3020 if ( !filter.isNull() )
3021 filters << '(' + filter + ')';
3022 }
3023
3024 return filters.join( QLatin1String( " AND " ) );
3025}
3026
3028{
3029 mExtraContextScope.reset( extraScope );
3030}
3031
3032void QgsAttributeForm::ContainerInformation::apply( QgsExpressionContext *expressionContext )
3033{
3034
3035 const bool newVisibility = expression.evaluate( expressionContext ).toBool();
3036
3037 if ( expression.isValid() && ! expression.hasEvalError() && newVisibility != isVisible )
3038 {
3039 if ( tabWidget )
3040 {
3041 tabWidget->setTabVisible( widget, newVisibility );
3042 }
3043 else
3044 {
3045 widget->setVisible( newVisibility );
3046 }
3047
3048 isVisible = newVisibility;
3049 }
3050
3051 const bool newCollapsedState = collapsedExpression.evaluate( expressionContext ).toBool();
3052
3053 if ( collapsedExpression.isValid() && ! collapsedExpression.hasEvalError() && newCollapsedState != isCollapsed )
3054 {
3055
3056 if ( QgsCollapsibleGroupBoxBasic * collapsibleGroupBox { qobject_cast<QgsCollapsibleGroupBoxBasic *>( widget ) } )
3057 {
3058 collapsibleGroupBox->setCollapsed( newCollapsedState );
3059 isCollapsed = newCollapsedState;
3060 }
3061 }
3062}
3063
3064void QgsAttributeForm::updateJoinedFields( const QgsEditorWidgetWrapper &eww )
3065{
3066 if ( !eww.layer()->fields().exists( eww.fieldIdx() ) )
3067 return;
3068
3069 QgsFeature formFeature;
3070 QgsField field = eww.layer()->fields().field( eww.fieldIdx() );
3071 QList<const QgsVectorLayerJoinInfo *> infos = eww.layer()->joinBuffer()->joinsWhereFieldIsId( field );
3072
3073 if ( infos.count() == 0 || !currentFormValuesFeature( formFeature ) )
3074 return;
3075
3076 const QString hint = tr( "No feature joined" );
3077 const auto constInfos = infos;
3078 for ( const QgsVectorLayerJoinInfo *info : constInfos )
3079 {
3080 if ( !info->isDynamicFormEnabled() )
3081 continue;
3082
3083 QgsFeature joinFeature = mLayer->joinBuffer()->joinedFeatureOf( info, formFeature );
3084
3085 mJoinedFeatures[info] = joinFeature;
3086
3087 if ( info->hasSubset() )
3088 {
3089 const QStringList subsetNames = QgsVectorLayerJoinInfo::joinFieldNamesSubset( *info );
3090
3091 const auto constSubsetNames = subsetNames;
3092 for ( const QString &field : constSubsetNames )
3093 {
3094 QString prefixedName = info->prefixedFieldName( field );
3095 QVariant val;
3096 QString hintText = hint;
3097
3098 if ( joinFeature.isValid() )
3099 {
3100 val = joinFeature.attribute( field );
3101 hintText.clear();
3102 }
3103
3104 changeAttribute( prefixedName, val, hintText );
3105 }
3106 }
3107 else
3108 {
3109 const QgsFields joinFields = joinFeature.fields();
3110 for ( const QgsField &field : joinFields )
3111 {
3112 QString prefixedName = info->prefixedFieldName( field );
3113 QVariant val;
3114 QString hintText = hint;
3115
3116 if ( joinFeature.isValid() )
3117 {
3118 val = joinFeature.attribute( field.name() );
3119 hintText.clear();
3120 }
3121
3122 changeAttribute( prefixedName, val, hintText );
3123 }
3124 }
3125 }
3126}
3127
3128bool QgsAttributeForm::fieldIsEditable( int fieldIndex ) const
3129{
3130 return QgsVectorLayerUtils::fieldIsEditable( mLayer, fieldIndex, mFeature );
3131}
3132
3133void QgsAttributeForm::updateFieldDependencies()
3134{
3135 mDefaultValueDependencies.clear();
3136 mVirtualFieldsDependencies.clear();
3137 mRelatedLayerFieldsDependencies.clear();
3138
3139 //create defaultValueDependencies
3140 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
3141 {
3142 QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
3143 if ( ! eww )
3144 continue;
3145
3146 updateFieldDependenciesDefaultValue( eww );
3147 updateFieldDependenciesVirtualFields( eww );
3148 updateRelatedLayerFieldsDependencies( eww );
3149 }
3150}
3151
3152void QgsAttributeForm::updateFieldDependenciesDefaultValue( QgsEditorWidgetWrapper *eww )
3153{
3155
3156 if ( exp.needsGeometry() )
3157 mNeedsGeometry = true;
3158
3159 //if a function requires all attributes, it should have the dependency of every field change
3160 if ( exp.referencedColumns().contains( QgsFeatureRequest::ALL_ATTRIBUTES ) )
3161 {
3162 const QList<int> allAttributeIds( mLayer->fields().allAttributesList() );
3163
3164 for ( const int id : allAttributeIds )
3165 {
3166 mDefaultValueDependencies.insertMulti( id, eww );
3167 }
3168 }
3169 else
3170 {
3171 //otherwise just enter for the field depending on
3172 const QSet<QString> referencedColumns = exp.referencedColumns();
3173 for ( const QString &referencedColumn : referencedColumns )
3174 {
3175 mDefaultValueDependencies.insertMulti( mLayer->fields().lookupField( referencedColumn ), eww );
3176 }
3177 }
3178}
3179
3180void QgsAttributeForm::updateFieldDependenciesVirtualFields( QgsEditorWidgetWrapper *eww )
3181{
3182 QString expressionField = eww->layer()->expressionField( eww->fieldIdx() );
3183 if ( expressionField.isEmpty() )
3184 return;
3185
3186 QgsExpression exp( expressionField );
3187
3188 if ( exp.needsGeometry() )
3189 mNeedsGeometry = true;
3190
3191 //if a function requires all attributes, it should have the dependency of every field change
3192 if ( exp.referencedColumns().contains( QgsFeatureRequest::ALL_ATTRIBUTES ) )
3193 {
3194 const QList<int> allAttributeIds( mLayer->fields().allAttributesList() );
3195
3196 for ( const int id : allAttributeIds )
3197 {
3198 mVirtualFieldsDependencies.insertMulti( id, eww );
3199 }
3200 }
3201 else
3202 {
3203 //otherwise just enter for the field depending on
3204 const QSet<QString> referencedColumns = exp.referencedColumns();
3205 for ( const QString &referencedColumn : referencedColumns )
3206 {
3207 mVirtualFieldsDependencies.insertMulti( mLayer->fields().lookupField( referencedColumn ), eww );
3208 }
3209 }
3210}
3211
3212void QgsAttributeForm::updateRelatedLayerFieldsDependencies( QgsEditorWidgetWrapper *eww )
3213{
3214 if ( eww )
3215 {
3216 QString expressionField = eww->layer()->expressionField( eww->fieldIdx() );
3217 if ( expressionField.contains( QStringLiteral( "relation_aggregate" ) )
3218 || expressionField.contains( QStringLiteral( "get_features" ) ) )
3219 mRelatedLayerFieldsDependencies.insert( eww );
3220 }
3221 else
3222 {
3223 mRelatedLayerFieldsDependencies.clear();
3224 //create defaultValueDependencies
3225 for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
3226 {
3227 QgsEditorWidgetWrapper *editorWidgetWrapper = qobject_cast<QgsEditorWidgetWrapper *>( ww );
3228 if ( ! editorWidgetWrapper )
3229 continue;
3230
3231 updateRelatedLayerFieldsDependencies( editorWidgetWrapper );
3232 }
3233 }
3234}
3235
3236void QgsAttributeForm::setMultiEditFeatureIdsRelations( const QgsFeatureIds &fids )
3237{
3238 for ( QgsAttributeFormWidget *formWidget : mFormWidgets )
3239 {
3240 QgsAttributeFormRelationEditorWidget *relationEditorWidget = dynamic_cast<QgsAttributeFormRelationEditorWidget *>( formWidget );
3241 if ( !relationEditorWidget )
3242 continue;
3243
3244 relationEditorWidget->setMultiEditFeatureIds( fids );
3245 }
3246}
3247
3248void QgsAttributeForm::updateIcon( QgsEditorWidgetWrapper *eww )
3249{
3250 if ( !eww->widget() || !mIconMap[eww->widget()] )
3251 return;
3252
3253 // no icon by default
3254 mIconMap[eww->widget()]->hide();
3255
3256 if ( !eww->widget()->isEnabled() && mLayer->isEditable() )
3257 {
3258 if ( mLayer->fields().fieldOrigin( eww->fieldIdx() ) == QgsFields::OriginJoin )
3259 {
3260 int srcFieldIndex;
3261 const QgsVectorLayerJoinInfo *info = mLayer->joinBuffer()->joinForFieldIndex( eww->fieldIdx(), mLayer->fields(), srcFieldIndex );
3262
3263 if ( !info )
3264 return;
3265
3266 if ( !info->isEditable() )
3267 {
3268 const QString file = QStringLiteral( "/mIconJoinNotEditable.svg" );
3269 const QString tooltip = tr( "Join settings do not allow editing" );
3270 reloadIcon( file, tooltip, mIconMap[eww->widget()] );
3271 }
3272 else if ( mMode == QgsAttributeEditorContext::AddFeatureMode && !info->hasUpsertOnEdit() )
3273 {
3274 const QString file = QStringLiteral( "mIconJoinHasNotUpsertOnEdit.svg" );
3275 const QString tooltip = tr( "Join settings do not allow upsert on edit" );
3276 reloadIcon( file, tooltip, mIconMap[eww->widget()] );
3277 }
3278 else if ( !info->joinLayer()->isEditable() )
3279 {
3280 const QString file = QStringLiteral( "/mIconJoinedLayerNotEditable.svg" );
3281 const QString tooltip = tr( "Joined layer is not toggled editable" );
3282 reloadIcon( file, tooltip, mIconMap[eww->widget()] );
3283 }
3284 }
3285 }
3286}
3287
3288void QgsAttributeForm::reloadIcon( const QString &file, const QString &tooltip, QSvgWidget *sw )
3289{
3290 sw->load( QgsApplication::iconPath( file ) );
3291 sw->setToolTip( tooltip );
3292 sw->show();
3293}
@ Row
A row of editors (horizontal layout)
@ File
Load the Python code from an external file.
@ Environment
Use the Python code available in the Python environment.
@ NoSource
Do not use Python code at all.
@ Dialog
Use the Python code provided in the dialog.
@ DragAndDrop
"Drag and drop" layout. Needs to be configured.
@ UiFile
Load a .ui file for the layout. Needs to be configured.
@ Action
A layer action element (since QGIS 3.22)
@ QmlElement
A QML element.
@ HtmlElement
A HTML element.
@ TextElement
A text element (since QGIS 3.30)
@ SpacerElement
A spacer element (since QGIS 3.30)
SelectBehavior
Specifies how a selection should be applied.
Definition: qgis.h:1358
@ SetSelection
Set selection, removing any existing selection.
@ AddToSelection
Add selection to current selection.
@ IntersectSelection
Modify current selection to include only select features which match.
@ RemoveFromSelection
Remove from current selection.
Wraps a button widget to launch a layer action.
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.
This element will load a layer action onto the form.
const QgsAction & action(const QgsVectorLayer *layer) const
Returns the (possibly lazy loaded) action for the given layer.
This is a container for attribute editors, used to group them visually in the attribute form if it is...
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.
This class contains context information for attribute editor widgets.
FormMode formMode() const
Returns the form mode.
QString attributeFormModeString() const
Returns given attributeFormMode as string.
@ Embed
A form was embedded as a widget on another form.
bool allowCustomUi() const
Returns true if the attribute editor should permit use of custom UI forms.
@ 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.
void setAttributeFormMode(const Mode &attributeFormMode)
Set attributeFormMode for the edited form.
This is 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.
This element will load a field's widget onto the form.
int idx() const
Returns the index of the field.
An attribute editor widget that will represent arbitrary HTML code.
QString htmlCode() const
The Html code that will be represented within this widget.
An attribute editor widget that will represent arbitrary QML code.
QString qmlCode() const
The QML code that will be represented within this widget.
This element will load a relation editor onto the form.
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.
An attribute editor widget that will represent a spacer.
bool drawLine() const
Returns true if the spacer element will contain an horizontal line.
An attribute editor widget that will represent arbitrary text code.
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...
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.
This class helps to support legacy open form scripts to be compatible with the new QgsAttributeForm s...
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() override
QgsAttributeForm(QgsVectorLayer *vl, const QgsFeature &feature=QgsFeature(), const QgsAttributeEditorContext &context=QgsAttributeEditorContext(), QWidget *parent=nullptr)
const QgsFeature & feature()
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.
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.
A vector of attributes.
Definition: qgsattributes.h:59
A groupbox that collapses/expands when toggled.
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.
A groupbox that collapses/expands when toggled and can save its collapsed and checked states.
Q_GADGET QString expression
Qgis::AttributeFormPythonInitCodeSource initCodeSource() const
Returns Python code source for edit form initialization (if it shall be loaded from a file,...
QString initCode() const
Gets Python code for edit form initialization.
QVariantMap widgetConfig(const QString &widgetName) const
Gets the configuration for the editor widget with the given name.
QString initFilePath() const
Gets Python external file path for edit form initialization.
QgsPropertyCollection dataDefinedFieldProperties(const QString &fieldName) const
Returns data defined properties for fieldName.
bool labelOnTop(int idx) const
If this returns true, the widget at the given index will receive its label on the previous line while...
QString uiForm() const
Returns the path or URL to the .ui form.
QString initFunction() const
Gets Python function for edit form initialization.
QList< QgsAttributeEditorElement * > tabs() const
Returns a list of tabs for EditorLayout::TabLayout obtained from the invisible root container.
Qgis::AttributeFormLayout layout() const
Gets the active layout style for the attribute editor for this layer.
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.
Holder for the widget type and its configuration for a field.
QVariantMap config() const
Manages an editor widget Widget and wrapper share the same parent.
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 * 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.
Class for parsing and evaluation of expressions (formerly called "search strings").
QSet< QString > referencedColumns() const
Gets list of columns referenced by the expression.
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.
This class 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:56
bool setAttribute(int field, const QVariant &attr)
Sets an attribute's value by field index.
Definition: qgsfeature.cpp:262
QgsAttributes attributes
Definition: qgsfeature.h:65
QgsFields fields
Definition: qgsfeature.h:66
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
Definition: qgsfeature.cpp:160
void setFields(const QgsFields &fields, bool initAttributes=false)
Assigns a field map with the feature to allow attribute access by attribute name.
Definition: qgsfeature.cpp:195
void setValid(bool validity)
Sets the validity of the feature.
Definition: qgsfeature.cpp:221
bool isValid() const
Returns the validity of this feature.
Definition: qgsfeature.cpp:216
QVariant attribute(const QString &name) const
Lookup attribute value by attribute name.
Definition: qgsfeature.cpp:335
void setGeometry(const QgsGeometry &geometry)
Set the feature's geometry.
Definition: qgsfeature.cpp:167
Q_GADGET QgsFeatureId id
Definition: qgsfeature.h:64
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)
Returns a HTML formatted tooltip string for a field, containing details like the field name,...
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:53
QString name
Definition: qgsfield.h:62
QString displayName() const
Returns the name to use when displaying this field.
Definition: qgsfield.cpp:88
QVariant::Type type
Definition: qgsfield.h:60
QgsDefaultValue defaultValueDefinition
Definition: qgsfield.h:64
QString comment
Definition: qgsfield.h:61
QgsFieldConstraints constraints
Definition: qgsfield.h:65
Container of fields for a vector layer.
Definition: qgsfields.h:45
QList< QgsField > toList() const
Utility function to return a list of QgsField instances.
Definition: qgsfields.cpp:212
QgsAttributeList allAttributesList() const
Utility function to get list of attribute indexes.
Definition: qgsfields.cpp:386
int indexFromName(const QString &fieldName) const
Gets the field index from the field name.
Definition: qgsfields.cpp:202
@ OriginJoin
Field comes from a joined layer (originIndex / 1000 = index of the join, originIndex % 1000 = index w...
Definition: qgsfields.h:52
int count() const
Returns number of items.
Definition: qgsfields.cpp:133
FieldOrigin fieldOrigin(int fieldIdx) const
Returns the field's origin (value from an enumeration).
Definition: qgsfields.cpp:189
QgsField field(int fieldIdx) const
Returns the field at particular index (must be in range 0..N-1).
Definition: qgsfields.cpp:168
bool exists(int i) const
Returns if a field index is valid.
Definition: qgsfields.cpp:153
int size() const
Returns number of items.
Definition: qgsfields.cpp:138
QgsField at(int i) const
Returns the field at particular index (must be in range 0..N-1).
Definition: qgsfields.cpp:163
int lookupField(const QString &fieldName) const
Looks up field's index from the field name.
Definition: qgsfields.cpp:359
A geometry is the spatial representation of a feature.
Definition: qgsgeometry.h:162
static QgsEditorWidgetRegistry * editorWidgetRegistry()
Returns the global editor widget registry, used for managing all known edit widget factories.
Definition: qgsgui.cpp:89
static bool pythonMacroAllowed(void(*lambda)()=nullptr, QgsMessageBar *messageBar=nullptr)
Returns true if python macros are currently allowed to be run If the global option is to ask user,...
Definition: qgsgui.cpp:353
Wraps a QQuickWidget to display HTML code.
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.
Definition: qgslogger.cpp:131
void editingStopped()
Emitted when edited changes have been successfully written to the data provider.
void editingStarted()
Emitted when editing on this layer has started.
void triggerRepaint(bool deferredUpdate=false)
Will advise the map canvas (and any other interested party) that this layer requires to be repainted.
Represents an item shown within a QgsMessageBar widget.
A bar for displaying non-blocking messages to the user.
Definition: qgsmessagebar.h:61
bool popWidget(QgsMessageBarItem *item)
Remove the specified item from the bar, and display the next most recent one in the stack.
void pushMessage(const QString &text, Qgis::MessageLevel level=Qgis::MessageLevel::Info, int duration=-1)
A convenience method for pushing a message with the specified text to the bar.
void pushItem(QgsMessageBarItem *item)
Display a message item on the bar, after hiding the currently visible one and putting it in a 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:86
T data() const
Access the payload data.
Definition: qgsoptional.h:113
QgsRelationManager * relationManager
Definition: qgsproject.h:117
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:481
bool hasProperty(int key) const final
Returns true if the collection contains a property with the specified key.
QgsProperty property(int key) const final
Returns a matching property from the collection, if one exists.
A store for object properties.
Definition: qgsproperty.h:228
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.
Wraps a QQuickWidget to display QML code.
void setQmlCode(const QString &qmlCode)
writes the qmlCode into a temporary file
This class manages a set of relations between layers.
QList< QgsRelation > referencedRelations(const QgsVectorLayer *layer=nullptr) const
Gets all relations where this layer is the referenced part (i.e.
Q_INVOKABLE QgsRelation relation(const QString &id) const
Gets access to a relation by its id.
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.
QString name
Definition: qgsrelation.h:48
Q_GADGET QString id
Definition: qgsrelation.h:45
bool isValid
Definition: qgsrelation.h:49
A QScrollArea subclass with improved scrolling behavior.
Definition: qgsscrollarea.h:41
Wraps a spacer widget.
void setDrawLine(bool drawLine)
Sets a flag to define if the spacer element will contain an horizontal line.
The QgsTabWidget class is the same as the QTabWidget but with additional methods to temporarily hide/...
Definition: qgstabwidget.h:32
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.
Wraps a text widget.
bool isInvalidJSON()
Returns whether the text edit widget contains Invalid JSON.
Wraps a label widget to display text.
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.
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.
QgsFeature joinedFeatureOf(const QgsVectorLayerJoinInfo *info, const QgsFeature &feature) const
Returns the joined feature corresponding to the feature.
Defines left outer join from our vector layer to some other vector layer.
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)
Tests whether a field is editable for a particular feature.
Represents a vector layer which manages a vector based data sets.
QString attributeDisplayName(int index) const
Convenience function that returns the attribute alias if defined or the field name else.
void beforeRemovingExpressionField(int idx)
Will be emitted, when an expression field is going to be deleted from this vector layer.
QgsFeatureIterator getFeatures(const QgsFeatureRequest &request=QgsFeatureRequest()) const FINAL
Queries the layer for features specified in request.
QgsFields fields() const FINAL
Returns the list of fields of this layer.
QString expressionField(int index) const
Returns the expression used for a given expression field.
int selectedFeatureCount() const
Returns the number of features that are selected in this layer.
Q_INVOKABLE void selectByExpression(const QString &expression, Qgis::SelectBehavior behavior=Qgis::SelectBehavior::SetSelection, QgsExpressionContext *context=nullptr)
Selects matching features using an expression.
void endEditCommand()
Finish edit command and add it to undo/redo stack.
void destroyEditCommand()
Destroy active command and reverts all changes in it.
Q_INVOKABLE const QgsFeatureIds & selectedFeatureIds() const
Returns a list of the selected features IDs in this layer.
bool changeAttributeValues(QgsFeatureId fid, const QgsAttributeMap &newValues, const QgsAttributeMap &oldValues=QgsAttributeMap(), bool skipDefaultValues=false)
Changes attributes' values for a feature (but does not immediately commit the changes).
bool isEditable() const FINAL
Returns true if the provider is in editing mode.
QgsVectorLayerJoinBuffer * joinBuffer()
Returns the join buffer object.
bool changeAttributeValue(QgsFeatureId fid, int field, const QVariant &newValue, const QVariant &oldValue=QVariant(), bool skipDefaultValues=false)
Changes an attribute value for a feature (but does not immediately commit the changes).
bool addFeature(QgsFeature &feature, QgsFeatureSink::Flags flags=QgsFeatureSink::Flags()) FINAL
Adds a single feature to the sink.
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.
QVariant defaultValue(int index, const QgsFeature &feature=QgsFeature(), QgsExpressionContext *context=nullptr) const
Returns the calculated default value for the specified field index.
void beginEditCommand(const QString &text)
Create edit command for undo/redo operations.
QgsEditFormConfig editFormConfig
void beforeModifiedCheck() const
Emitted when the layer is checked for modifications. Use for last-minute additions.
Manages an editor widget Widget and wrapper share the same parent.
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:247
#define Q_NOWARN_DEPRECATED_POP
Definition: qgis.h:5776
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:5775
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition: qgis.h:5111
QMap< int, QVariant > QgsAttributeMap
Definition: qgsattributes.h:42
QSet< QgsFeatureId > QgsFeatureIds
Definition: qgsfeatureid.h:37
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
Definition: qgsfeatureid.h:28
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugError(str)
Definition: qgslogger.h:38