1 /***************************************************************************
2     qgseditformconfig.cpp
3     ---------------------
4     begin                : November 2015
5     copyright            : (C) 2015 by 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 "qgseditformconfig_p.h"
17 #include "qgseditformconfig.h"
18 #include "qgsnetworkcontentfetcherregistry.h"
19 #include "qgspathresolver.h"
20 #include "qgsproject.h"
21 #include "qgsreadwritecontext.h"
22 #include "qgsrelationmanager.h"
23 #include "qgslogger.h"
24 #include "qgsxmlutils.h"
25 #include "qgsapplication.h"
26 #include "qgsmessagelog.h"
27 #include "qgsattributeeditorcontainer.h"
28 #include "qgsattributeeditorfield.h"
29 #include "qgsattributeeditorrelation.h"
30 #include <QUrl>
31 
QgsEditFormConfig()32 QgsEditFormConfig::QgsEditFormConfig()
33   : d( new QgsEditFormConfigPrivate() )
34 {
35 }
36 
setDataDefinedFieldProperties(const QString & fieldName,const QgsPropertyCollection & properties)37 void QgsEditFormConfig::setDataDefinedFieldProperties( const QString &fieldName, const QgsPropertyCollection &properties )
38 {
39   d.detach();
40   d->mDataDefinedFieldProperties[ fieldName ] = properties;
41 }
42 
dataDefinedFieldProperties(const QString & fieldName) const43 QgsPropertyCollection QgsEditFormConfig::dataDefinedFieldProperties( const QString &fieldName ) const
44 {
45   return d->mDataDefinedFieldProperties.value( fieldName );
46 }
47 
propertyDefinitions()48 const QgsPropertiesDefinition &QgsEditFormConfig::propertyDefinitions()
49 {
50   return QgsEditFormConfigPrivate::propertyDefinitions();
51 }
52 
widgetConfig(const QString & widgetName) const53 QVariantMap QgsEditFormConfig::widgetConfig( const QString &widgetName ) const
54 {
55   const int fieldIndex = d->mFields.indexOf( widgetName );
56   if ( fieldIndex != -1 )
57     return d->mFields.at( fieldIndex ).editorWidgetSetup().config();
58   else
59     return d->mWidgetConfigs.value( widgetName );
60 }
61 
setFields(const QgsFields & fields)62 void QgsEditFormConfig::setFields( const QgsFields &fields )
63 {
64   d.detach();
65   d->mFields = fields;
66 
67   if ( !d->mConfiguredRootContainer )
68   {
69     d->mInvisibleRootContainer->clear();
70     for ( int i = 0; i < d->mFields.size(); ++i )
71     {
72       QgsAttributeEditorField *field = new QgsAttributeEditorField( d->mFields.at( i ).name(), i, d->mInvisibleRootContainer );
73       d->mInvisibleRootContainer->addChildElement( field );
74     }
75   }
76 }
77 
onRelationsLoaded()78 void QgsEditFormConfig::onRelationsLoaded()
79 {
80   const QList<QgsAttributeEditorElement *> relations = d->mInvisibleRootContainer->findElements( QgsAttributeEditorElement::AeTypeRelation );
81 
82   for ( QgsAttributeEditorElement *relElem : relations )
83   {
84     QgsAttributeEditorRelation *rel = dynamic_cast< QgsAttributeEditorRelation * >( relElem );
85     if ( !rel )
86       continue;
87 
88     rel->init( QgsProject::instance()->relationManager() );
89   }
90 }
91 
legacyUpdateRelationWidgetInTabs(QgsAttributeEditorContainer * container,const QString & widgetName,const QVariantMap & config)92 bool QgsEditFormConfig::legacyUpdateRelationWidgetInTabs( QgsAttributeEditorContainer *container,  const QString &widgetName, const QVariantMap &config )
93 {
94   const QList<QgsAttributeEditorElement *> children = container->children();
95   for ( QgsAttributeEditorElement *child : children )
96   {
97     if ( child->type() ==  QgsAttributeEditorElement::AeTypeContainer )
98     {
99       QgsAttributeEditorContainer *container = dynamic_cast<QgsAttributeEditorContainer *>( child );
100       if ( legacyUpdateRelationWidgetInTabs( container, widgetName, config ) )
101       {
102         //return when a relation has been set in a child or child child...
103         return true;
104       }
105     }
106     else if ( child->type() ==  QgsAttributeEditorElement::AeTypeRelation )
107     {
108       QgsAttributeEditorRelation *relation = dynamic_cast< QgsAttributeEditorRelation * >( child );
109       if ( relation )
110       {
111         if ( relation->relation().id() == widgetName )
112         {
113           if ( config.contains( QStringLiteral( "nm-rel" ) ) )
114           {
115             relation->setNmRelationId( config[QStringLiteral( "nm-rel" )] );
116           }
117           if ( config.contains( QStringLiteral( "force-suppress-popup" ) ) )
118           {
119             relation->setForceSuppressFormPopup( config[QStringLiteral( "force-suppress-popup" )].toBool() );
120           }
121           return true;
122         }
123       }
124     }
125   }
126   return false;
127 }
128 
setWidgetConfig(const QString & widgetName,const QVariantMap & config)129 bool QgsEditFormConfig::setWidgetConfig( const QString &widgetName, const QVariantMap &config )
130 {
131   if ( d->mFields.indexOf( widgetName ) != -1 )
132   {
133     QgsDebugMsg( QStringLiteral( "Trying to set a widget config for a field on QgsEditFormConfig. Use layer->setEditorWidgetSetup() instead." ) );
134     return false;
135   }
136 
137   //for legacy use it writes the relation editor configuration into the first instance of the widget
138   if ( config.contains( QStringLiteral( "force-suppress-popup" ) ) || config.contains( QStringLiteral( "nm-rel" ) ) )
139   {
140     QgsMessageLog::logMessage( QStringLiteral( "Deprecation Warning: Trying to set a relation config directly on the relation %1. Relation settings should be done for the specific widget instance instead. Use attributeEditorRelation->setNmRelationId() or attributeEditorRelation->setForceSuppressFormPopup() instead." ).arg( widgetName ) );
141     legacyUpdateRelationWidgetInTabs( d->mInvisibleRootContainer, widgetName, config );
142   }
143 
144   d.detach();
145   d->mWidgetConfigs[widgetName] = config;
146   return true;
147 }
148 
removeWidgetConfig(const QString & widgetName)149 bool QgsEditFormConfig::removeWidgetConfig( const QString &widgetName )
150 {
151   d.detach();
152   return d->mWidgetConfigs.remove( widgetName ) != 0;
153 }
154 
QgsEditFormConfig(const QgsEditFormConfig & o)155 QgsEditFormConfig::QgsEditFormConfig( const QgsEditFormConfig &o ) //NOLINT
156   : d( o.d )
157 {
158 }
159 
~QgsEditFormConfig()160 QgsEditFormConfig::~QgsEditFormConfig() //NOLINT
161 {}
162 
operator =(const QgsEditFormConfig & o)163 QgsEditFormConfig &QgsEditFormConfig::operator=( const QgsEditFormConfig &o )  //NOLINT
164 {
165   d = o.d;
166   return *this;
167 }
168 
operator ==(const QgsEditFormConfig & o)169 bool QgsEditFormConfig::operator==( const QgsEditFormConfig &o )
170 {
171   return d == o.d;
172 }
173 
addTab(QgsAttributeEditorElement * data)174 void QgsEditFormConfig::addTab( QgsAttributeEditorElement *data )
175 {
176   d.detach();
177   d->mInvisibleRootContainer->addChildElement( data );
178 }
179 
tabs() const180 QList<QgsAttributeEditorElement *> QgsEditFormConfig::tabs() const
181 {
182   return d->mInvisibleRootContainer->children();
183 }
184 
clearTabs()185 void QgsEditFormConfig::clearTabs()
186 {
187   d.detach();
188   d->mInvisibleRootContainer->clear();
189 }
190 
invisibleRootContainer()191 QgsAttributeEditorContainer *QgsEditFormConfig::invisibleRootContainer()
192 {
193   return d->mInvisibleRootContainer;
194 }
195 
layout() const196 QgsEditFormConfig::EditorLayout QgsEditFormConfig::layout() const
197 {
198   return d->mEditorLayout;
199 }
200 
setLayout(QgsEditFormConfig::EditorLayout editorLayout)201 void QgsEditFormConfig::setLayout( QgsEditFormConfig::EditorLayout editorLayout )
202 {
203   d.detach();
204   d->mEditorLayout = editorLayout;
205 
206   if ( editorLayout == TabLayout )
207     d->mConfiguredRootContainer = true;
208 }
209 
uiForm() const210 QString QgsEditFormConfig::uiForm() const
211 {
212   return d->mUiFormPath;
213 }
214 
setUiForm(const QString & ui)215 void QgsEditFormConfig::setUiForm( const QString &ui )
216 {
217   if ( !ui.isEmpty() && !QUrl::fromUserInput( ui ).isLocalFile() )
218   {
219     // any existing download will not be restarted!
220     QgsApplication::instance()->networkContentFetcherRegistry()->fetch( ui, Qgis::ActionStart::Immediate );
221   }
222 
223   if ( ui.isEmpty() )
224   {
225     setLayout( GeneratedLayout );
226   }
227   else
228   {
229     setLayout( UiFileLayout );
230   }
231   d->mUiFormPath = ui;
232 }
233 
readOnly(int idx) const234 bool QgsEditFormConfig::readOnly( int idx ) const
235 {
236   if ( idx >= 0 && idx < d->mFields.count() )
237   {
238     if ( d->mFields.fieldOrigin( idx ) == QgsFields::OriginJoin
239          || d->mFields.fieldOrigin( idx ) == QgsFields::OriginExpression )
240       return true;
241     return !d->mFieldEditables.value( d->mFields.at( idx ).name(), true );
242   }
243   else
244     return false;
245 }
246 
labelOnTop(int idx) const247 bool QgsEditFormConfig::labelOnTop( int idx ) const
248 {
249   if ( idx >= 0 && idx < d->mFields.count() )
250     return d->mLabelOnTop.value( d->mFields.at( idx ).name(), false );
251   else
252     return false;
253 }
254 
setReadOnly(int idx,bool readOnly)255 void QgsEditFormConfig::setReadOnly( int idx, bool readOnly )
256 {
257   if ( idx >= 0 && idx < d->mFields.count() )
258   {
259     d.detach();
260     d->mFieldEditables[ d->mFields.at( idx ).name()] = !readOnly;
261   }
262 }
263 
setLabelOnTop(int idx,bool onTop)264 void QgsEditFormConfig::setLabelOnTop( int idx, bool onTop )
265 {
266   if ( idx >= 0 && idx < d->mFields.count() )
267   {
268     d.detach();
269     d->mLabelOnTop[ d->mFields.at( idx ).name()] = onTop;
270   }
271 }
272 
reuseLastValue(int index) const273 bool QgsEditFormConfig::reuseLastValue( int index ) const
274 {
275   if ( index >= 0 && index < d->mFields.count() )
276     return d->mReuseLastValue.value( d->mFields.at( index ).name(), false );
277   else
278     return false;
279 }
280 
setReuseLastValue(int index,bool reuse)281 void QgsEditFormConfig::setReuseLastValue( int index, bool reuse )
282 {
283   if ( index >= 0 && index < d->mFields.count() )
284   {
285     d.detach();
286     d->mReuseLastValue[ d->mFields.at( index ).name()] = reuse;
287   }
288 }
289 
initFunction() const290 QString QgsEditFormConfig::initFunction() const
291 {
292   return d->mInitFunction;
293 }
294 
setInitFunction(const QString & function)295 void QgsEditFormConfig::setInitFunction( const QString &function )
296 {
297   d.detach();
298   d->mInitFunction = function;
299 }
300 
initCode() const301 QString QgsEditFormConfig::initCode() const
302 {
303   return d->mInitCode;
304 }
305 
setInitCode(const QString & code)306 void QgsEditFormConfig::setInitCode( const QString &code )
307 {
308   d.detach();
309   d->mInitCode = code;
310 }
311 
initFilePath() const312 QString QgsEditFormConfig::initFilePath() const
313 {
314   return d->mInitFilePath;
315 }
316 
setInitFilePath(const QString & filePath)317 void QgsEditFormConfig::setInitFilePath( const QString &filePath )
318 {
319   d.detach();
320   d->mInitFilePath = filePath;
321 
322   // if this is an URL, download file as there is a good chance it will be used later
323   if ( !filePath.isEmpty() && !QUrl::fromUserInput( filePath ).isLocalFile() )
324   {
325     // any existing download will not be restarted!
326     QgsApplication::instance()->networkContentFetcherRegistry()->fetch( filePath, Qgis::ActionStart::Immediate );
327   }
328 }
329 
initCodeSource() const330 QgsEditFormConfig::PythonInitCodeSource QgsEditFormConfig::initCodeSource() const
331 {
332   return d->mInitCodeSource;
333 }
334 
setInitCodeSource(const QgsEditFormConfig::PythonInitCodeSource initCodeSource)335 void QgsEditFormConfig::setInitCodeSource( const QgsEditFormConfig::PythonInitCodeSource initCodeSource )
336 {
337   d.detach();
338   d->mInitCodeSource = initCodeSource;
339 }
340 
suppress() const341 QgsEditFormConfig::FeatureFormSuppress QgsEditFormConfig::suppress() const
342 {
343   return d->mSuppressForm;
344 }
345 
setSuppress(QgsEditFormConfig::FeatureFormSuppress s)346 void QgsEditFormConfig::setSuppress( QgsEditFormConfig::FeatureFormSuppress s )
347 {
348   d.detach();
349   d->mSuppressForm = s;
350 }
351 
readXml(const QDomNode & node,QgsReadWriteContext & context)352 void QgsEditFormConfig::readXml( const QDomNode &node, QgsReadWriteContext &context )
353 {
354   const QgsReadWriteContextCategoryPopper p = context.enterCategory( QObject::tr( "Edit form config" ) );
355 
356   d.detach();
357 
358   const QDomNode editFormNode = node.namedItem( QStringLiteral( "editform" ) );
359   if ( !editFormNode.isNull() )
360   {
361     const QDomElement e = editFormNode.toElement();
362     const bool tolerantRemoteUrls = e.hasAttribute( QStringLiteral( "tolerant" ) );
363     if ( !e.text().isEmpty() )
364     {
365       const QString uiFormPath = context.pathResolver().readPath( e.text() );
366       // <= 3.2 had a bug where invalid ui paths would get written into projects on load
367       // to avoid restoring these invalid paths, we take a less-tolerant approach for older (untrustworthy) projects
368       // and only set ui forms paths IF they are local files OR start with "http(s)".
369       const bool localFile = QFileInfo::exists( uiFormPath );
370       if ( localFile || tolerantRemoteUrls || uiFormPath.startsWith( QLatin1String( "http" ) ) )
371         setUiForm( uiFormPath );
372     }
373   }
374 
375   const QDomNode editFormInitNode = node.namedItem( QStringLiteral( "editforminit" ) );
376   if ( !editFormInitNode.isNull() )
377   {
378     d->mInitFunction = editFormInitNode.toElement().text();
379   }
380 
381   const QDomNode editFormInitCodeSourceNode = node.namedItem( QStringLiteral( "editforminitcodesource" ) );
382   if ( !editFormInitCodeSourceNode.isNull() && !editFormInitCodeSourceNode.toElement().text().isEmpty() )
383   {
384     setInitCodeSource( static_cast< QgsEditFormConfig::PythonInitCodeSource >( editFormInitCodeSourceNode.toElement().text().toInt() ) );
385   }
386 
387   const QDomNode editFormInitCodeNode = node.namedItem( QStringLiteral( "editforminitcode" ) );
388   if ( !editFormInitCodeNode.isNull() )
389   {
390     setInitCode( editFormInitCodeNode.toElement().text() );
391   }
392 
393   // Temporary < 2.12 b/w compatibility "dot" support patch
394   // \see: https://github.com/qgis/QGIS/pull/2498
395   // For b/w compatibility, check if there's a dot in the function name
396   // and if yes, transform it in an import statement for the module
397   // and set the PythonInitCodeSource to CodeSourceDialog
398   const int dotPos = d->mInitFunction.lastIndexOf( '.' );
399   if ( dotPos >= 0 ) // It's a module
400   {
401     setInitCodeSource( QgsEditFormConfig::CodeSourceDialog );
402     setInitCode( QStringLiteral( "from %1 import %2\n" ).arg( d->mInitFunction.left( dotPos ), d->mInitFunction.mid( dotPos + 1 ) ) );
403     setInitFunction( d->mInitFunction.mid( dotPos + 1 ) );
404   }
405 
406   const QDomNode editFormInitFilePathNode = node.namedItem( QStringLiteral( "editforminitfilepath" ) );
407   if ( !editFormInitFilePathNode.isNull() && !editFormInitFilePathNode.toElement().text().isEmpty() )
408   {
409     setInitFilePath( context.pathResolver().readPath( editFormInitFilePathNode.toElement().text() ) );
410   }
411 
412   const QDomNode fFSuppNode = node.namedItem( QStringLiteral( "featformsuppress" ) );
413   if ( fFSuppNode.isNull() )
414   {
415     d->mSuppressForm = QgsEditFormConfig::SuppressDefault;
416   }
417   else
418   {
419     const QDomElement e = fFSuppNode.toElement();
420     d->mSuppressForm = static_cast< QgsEditFormConfig::FeatureFormSuppress >( e.text().toInt() );
421   }
422 
423   // tab display
424   const QDomNode editorLayoutNode = node.namedItem( QStringLiteral( "editorlayout" ) );
425   if ( editorLayoutNode.isNull() )
426   {
427     d->mEditorLayout = QgsEditFormConfig::GeneratedLayout;
428   }
429   else
430   {
431     if ( editorLayoutNode.toElement().text() == QLatin1String( "uifilelayout" ) )
432     {
433       d->mEditorLayout = QgsEditFormConfig::UiFileLayout;
434     }
435     else if ( editorLayoutNode.toElement().text() == QLatin1String( "tablayout" ) )
436     {
437       d->mEditorLayout = QgsEditFormConfig::TabLayout;
438     }
439     else
440     {
441       d->mEditorLayout = QgsEditFormConfig::GeneratedLayout;
442     }
443   }
444 
445   d->mFieldEditables.clear();
446   const QDomNodeList editableNodeList = node.namedItem( QStringLiteral( "editable" ) ).toElement().childNodes();
447   for ( int i = 0; i < editableNodeList.size(); ++i )
448   {
449     const QDomElement editableElement = editableNodeList.at( i ).toElement();
450     d->mFieldEditables.insert( editableElement.attribute( QStringLiteral( "name" ) ), static_cast< bool >( editableElement.attribute( QStringLiteral( "editable" ) ).toInt() ) );
451   }
452 
453   d->mLabelOnTop.clear();
454   const QDomNodeList labelOnTopNodeList = node.namedItem( QStringLiteral( "labelOnTop" ) ).toElement().childNodes();
455   for ( int i = 0; i < labelOnTopNodeList.size(); ++i )
456   {
457     const QDomElement labelOnTopElement = labelOnTopNodeList.at( i ).toElement();
458     d->mLabelOnTop.insert( labelOnTopElement.attribute( QStringLiteral( "name" ) ), static_cast< bool >( labelOnTopElement.attribute( QStringLiteral( "labelOnTop" ) ).toInt() ) );
459   }
460 
461   d->mReuseLastValue.clear();
462   const QDomNodeList reuseLastValueNodeList = node.namedItem( QStringLiteral( "reuseLastValue" ) ).toElement().childNodes();
463   for ( int i = 0; i < reuseLastValueNodeList.size(); ++i )
464   {
465     const QDomElement reuseLastValueElement = reuseLastValueNodeList.at( i ).toElement();
466     d->mReuseLastValue.insert( reuseLastValueElement.attribute( QStringLiteral( "name" ) ), static_cast< bool >( reuseLastValueElement.attribute( QStringLiteral( "reuseLastValue" ) ).toInt() ) );
467   }
468 
469   // Read data defined field properties
470   const QDomNodeList fieldDDPropertiesNodeList = node.namedItem( QStringLiteral( "dataDefinedFieldProperties" ) ).toElement().childNodes();
471   for ( int i = 0; i < fieldDDPropertiesNodeList.size(); ++i )
472   {
473     const QDomElement DDElement = fieldDDPropertiesNodeList.at( i ).toElement();
474     QgsPropertyCollection collection;
475     collection.readXml( DDElement, propertyDefinitions() );
476     d->mDataDefinedFieldProperties.insert( DDElement.attribute( QStringLiteral( "name" ) ), collection );
477   }
478 
479   const QDomNodeList widgetsNodeList = node.namedItem( QStringLiteral( "widgets" ) ).toElement().childNodes();
480 
481   for ( int i = 0; i < widgetsNodeList.size(); ++i )
482   {
483     const QDomElement widgetElement = widgetsNodeList.at( i ).toElement();
484     const QVariant config = QgsXmlUtils::readVariant( widgetElement.firstChildElement( QStringLiteral( "config" ) ) );
485 
486     d->mWidgetConfigs[widgetElement.attribute( QStringLiteral( "name" ) )] = config.toMap();
487   }
488 
489   // tabs and groups display info
490   const QDomNode attributeEditorFormNode = node.namedItem( QStringLiteral( "attributeEditorForm" ) );
491   if ( !attributeEditorFormNode.isNull() )
492   {
493     const QDomNodeList attributeEditorFormNodeList = attributeEditorFormNode.toElement().childNodes();
494 
495     if ( attributeEditorFormNodeList.size() )
496     {
497       d->mConfiguredRootContainer = true;
498       clearTabs();
499 
500       for ( int i = 0; i < attributeEditorFormNodeList.size(); i++ )
501       {
502         QDomElement elem = attributeEditorFormNodeList.at( i ).toElement();
503 
504         fixLegacyConfig( elem );
505 
506         const QString layerId = node.namedItem( QStringLiteral( "id" ) ).toElement().text();
507         QgsAttributeEditorElement *attributeEditorWidget = QgsAttributeEditorElement::create( elem, layerId, d->mFields, context, nullptr );
508         if ( attributeEditorWidget )
509           addTab( attributeEditorWidget );
510       }
511 
512       onRelationsLoaded();
513     }
514   }
515 }
516 
fixLegacyConfig(QDomElement & el)517 void QgsEditFormConfig::fixLegacyConfig( QDomElement &el )
518 {
519   // recursive method to move widget config into attribute element config
520 
521   if ( el.tagName() == QLatin1String( "attributeEditorRelation" ) )
522   {
523     if ( !el.hasAttribute( QStringLiteral( "forceSuppressFormPopup" ) ) )
524     {
525       // pre QGIS 3.16 compatibility - the widgets section is read before
526       const bool forceSuppress = widgetConfig( el.attribute( QStringLiteral( "relation" ) ) ).value( QStringLiteral( "force-suppress-popup" ), false ).toBool();
527       el.setAttribute( QStringLiteral( "forceSuppressFormPopup" ), forceSuppress ? 1 : 0 );
528     }
529     if ( !el.hasAttribute( QStringLiteral( "nmRelationId" ) ) )
530     {
531       // pre QGIS 3.16 compatibility - the widgets section is read before
532       el.setAttribute( QStringLiteral( "nmRelationId" ), widgetConfig( el.attribute( QStringLiteral( "relation" ) ) ).value( QStringLiteral( "nm-rel" ) ).toString() );
533     }
534   }
535 
536   const QDomNodeList children = el.childNodes();
537   for ( int i = 0; i < children.size(); i++ )
538   {
539     QDomElement child = children.at( i ).toElement();
540     fixLegacyConfig( child );
541     el.replaceChild( child, children.at( i ) );
542   }
543 }
544 
writeXml(QDomNode & node,const QgsReadWriteContext & context) const545 void QgsEditFormConfig::writeXml( QDomNode &node, const QgsReadWriteContext &context ) const
546 {
547   QDomDocument doc( node.ownerDocument() );
548 
549   QDomElement efField  = doc.createElement( QStringLiteral( "editform" ) );
550   efField.setAttribute( QStringLiteral( "tolerant" ), QStringLiteral( "1" ) );
551   const QDomText efText = doc.createTextNode( context.pathResolver().writePath( uiForm() ) );
552   efField.appendChild( efText );
553   node.appendChild( efField );
554 
555   QDomElement efiField  = doc.createElement( QStringLiteral( "editforminit" ) );
556   if ( !initFunction().isEmpty() )
557     efiField.appendChild( doc.createTextNode( initFunction() ) );
558   node.appendChild( efiField );
559 
560   QDomElement eficsField  = doc.createElement( QStringLiteral( "editforminitcodesource" ) );
561   eficsField.appendChild( doc.createTextNode( QString::number( initCodeSource() ) ) );
562   node.appendChild( eficsField );
563 
564   QDomElement efifpField  = doc.createElement( QStringLiteral( "editforminitfilepath" ) );
565   efifpField.appendChild( doc.createTextNode( context.pathResolver().writePath( initFilePath() ) ) );
566   node.appendChild( efifpField );
567 
568   QDomElement eficField  = doc.createElement( QStringLiteral( "editforminitcode" ) );
569   eficField.appendChild( doc.createCDATASection( initCode() ) );
570   node.appendChild( eficField );
571 
572   QDomElement fFSuppElem  = doc.createElement( QStringLiteral( "featformsuppress" ) );
573   const QDomText fFSuppText = doc.createTextNode( QString::number( suppress() ) );
574   fFSuppElem.appendChild( fFSuppText );
575   node.appendChild( fFSuppElem );
576 
577   // tab display
578   QDomElement editorLayoutElem  = doc.createElement( QStringLiteral( "editorlayout" ) );
579   switch ( layout() )
580   {
581     case QgsEditFormConfig::UiFileLayout:
582       editorLayoutElem.appendChild( doc.createTextNode( QStringLiteral( "uifilelayout" ) ) );
583       break;
584 
585     case QgsEditFormConfig::TabLayout:
586       editorLayoutElem.appendChild( doc.createTextNode( QStringLiteral( "tablayout" ) ) );
587       break;
588 
589     case QgsEditFormConfig::GeneratedLayout:
590     default:
591       editorLayoutElem.appendChild( doc.createTextNode( QStringLiteral( "generatedlayout" ) ) );
592       break;
593   }
594 
595   node.appendChild( editorLayoutElem );
596 
597   // tabs and groups of edit form
598   if ( !tabs().empty() && d->mConfiguredRootContainer )
599   {
600     QDomElement tabsElem = doc.createElement( QStringLiteral( "attributeEditorForm" ) );
601     const QDomElement rootElem = d->mInvisibleRootContainer->toDomElement( doc );
602     const QDomNodeList elemList = rootElem.childNodes();
603     while ( !elemList.isEmpty() )
604     {
605       tabsElem.appendChild( elemList.at( 0 ) );
606     }
607     node.appendChild( tabsElem );
608   }
609 
610   QDomElement editableElem = doc.createElement( QStringLiteral( "editable" ) );
611   for ( auto editIt = d->mFieldEditables.constBegin(); editIt != d->mFieldEditables.constEnd(); ++editIt )
612   {
613     QDomElement fieldElem = doc.createElement( QStringLiteral( "field" ) );
614     fieldElem.setAttribute( QStringLiteral( "name" ), editIt.key() );
615     fieldElem.setAttribute( QStringLiteral( "editable" ), editIt.value() ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
616     editableElem.appendChild( fieldElem );
617   }
618   node.appendChild( editableElem );
619 
620   QDomElement labelOnTopElem = doc.createElement( QStringLiteral( "labelOnTop" ) );
621   for ( auto labelOnTopIt = d->mLabelOnTop.constBegin(); labelOnTopIt != d->mLabelOnTop.constEnd(); ++labelOnTopIt )
622   {
623     QDomElement fieldElem = doc.createElement( QStringLiteral( "field" ) );
624     fieldElem.setAttribute( QStringLiteral( "name" ), labelOnTopIt.key() );
625     fieldElem.setAttribute( QStringLiteral( "labelOnTop" ), labelOnTopIt.value() ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
626     labelOnTopElem.appendChild( fieldElem );
627   }
628   node.appendChild( labelOnTopElem );
629 
630   QDomElement reuseLastValueElem = doc.createElement( QStringLiteral( "reuseLastValue" ) );
631   for ( auto reuseLastValueIt = d->mReuseLastValue.constBegin(); reuseLastValueIt != d->mReuseLastValue.constEnd(); ++reuseLastValueIt )
632   {
633     QDomElement fieldElem = doc.createElement( QStringLiteral( "field" ) );
634     fieldElem.setAttribute( QStringLiteral( "name" ), reuseLastValueIt.key() );
635     fieldElem.setAttribute( QStringLiteral( "reuseLastValue" ), reuseLastValueIt.value() ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
636     reuseLastValueElem.appendChild( fieldElem );
637   }
638   node.appendChild( reuseLastValueElem );
639 
640   // Store data defined field properties
641   QDomElement ddFieldPropsElement = doc.createElement( QStringLiteral( "dataDefinedFieldProperties" ) );
642   for ( auto it = d->mDataDefinedFieldProperties.constBegin(); it != d->mDataDefinedFieldProperties.constEnd(); ++it )
643   {
644     QDomElement ddPropsElement = doc.createElement( QStringLiteral( "field" ) );
645     ddPropsElement.setAttribute( QStringLiteral( "name" ), it.key() );
646     it.value().writeXml( ddPropsElement, propertyDefinitions() );
647     ddFieldPropsElement.appendChild( ddPropsElement );
648   }
649   node.appendChild( ddFieldPropsElement );
650 
651   QDomElement widgetsElem = doc.createElement( QStringLiteral( "widgets" ) );
652 
653   QMap<QString, QVariantMap >::ConstIterator configIt( d->mWidgetConfigs.constBegin() );
654 
655   while ( configIt != d->mWidgetConfigs.constEnd() )
656   {
657     QDomElement widgetElem = doc.createElement( QStringLiteral( "widget" ) );
658     widgetElem.setAttribute( QStringLiteral( "name" ), configIt.key() );
659     // widgetElem.setAttribute( "notNull",  );
660 
661     QDomElement configElem = QgsXmlUtils::writeVariant( configIt.value(), doc );
662     configElem.setTagName( QStringLiteral( "config" ) );
663     widgetElem.appendChild( configElem );
664     widgetsElem.appendChild( widgetElem );
665     ++configIt;
666   }
667 
668   node.appendChild( widgetsElem );
669 }
670 
attributeEditorElementFromDomElement(QDomElement & elem,QgsAttributeEditorElement * parent,const QString & layerId,const QgsReadWriteContext & context)671 QgsAttributeEditorElement *QgsEditFormConfig::attributeEditorElementFromDomElement( QDomElement &elem, QgsAttributeEditorElement *parent, const QString &layerId, const QgsReadWriteContext &context )
672 {
673   return QgsAttributeEditorElement::create( elem, layerId, d->mFields, context, parent );
674 }
675