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