From a2e5f24fa0c1a5eb61d2f575679972ea25391454 Mon Sep 17 00:00:00 2001 From: David Signer Date: Mon, 6 Jan 2020 14:38:41 +0100 Subject: [PATCH 01/10] hack to check behavior if copy a non valid feature against hard constraints --- src/app/qgisapp.cpp | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index b9ade43c272e..c3fde6643433 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -87,6 +87,7 @@ #include "qgssourceselectproviderregistry.h" #include "qgssourceselectprovider.h" #include "qgsprovidermetadata.h" +#include "qgsattributedialog.h" #include "qgsanalysis.h" #include "qgsgeometrycheckregistry.h" @@ -9588,6 +9589,45 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer ) // now create new feature using pasted feature as a template. This automatically handles default // values and field constraints QgsFeatureList newFeatures {QgsVectorLayerUtils::createFeatures( pasteVectorLayer, newFeaturesDataList, &context )}; + + // check against hard constraints + for ( QgsFeature &f : newFeatures ) + { + bool valid = true; + for ( int idx = 0; idx < pasteVectorLayer->fields().count(); ++idx ) + { + QStringList errors; + valid = QgsVectorLayerUtils::validateAttribute( pasteVectorLayer, f, idx, errors, QgsFieldConstraints::ConstraintStrengthHard, QgsFieldConstraints::ConstraintOriginNotSet ); + if ( !valid ) + { + break; + } + } + + //open attribute form including option "cancel copy all" / "cancel copy of invalid" / "store invalid features" + if ( !valid ) + { + //QgsFeatureAction action( tr( "Fix feature" ), f, pasteVectorLayer, QString(), -1, QgisApp::instance() ); + //action.editFeature( true ); + QgsAttributeDialog *dialog = new QgsAttributeDialog( pasteVectorLayer, &f, false ); + + if ( !f.isValid() ) + dialog->setMode( QgsAttributeEditorContext::AddFeatureMode ); + + int feedback = dialog->exec(); + + if ( feedback > 0 ) + { + f.setAttributes( dialog->feature()->attributes() ); + } + else + { + visibleMessageBar()->pushMessage( tr( "Paste features" ), tr( "Do not copy feature" ), Qgis::Warning, messageTimeout() ); + newFeatures.removeOne( f ); + } + } + } + pasteVectorLayer->addFeatures( newFeatures ); QgsFeatureIds newIds; newIds.reserve( newFeatures.size() ); @@ -9600,7 +9640,7 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer ) pasteVectorLayer->endEditCommand(); pasteVectorLayer->updateExtents(); - int nCopiedFeatures = features.count(); + int nCopiedFeatures = newFeatures.count(); Qgis::MessageLevel level = ( nCopiedFeatures == 0 || nCopiedFeatures < nTotalFeatures || invalidGeometriesCount > 0 ) ? Qgis::Warning : Qgis::Info; QString message; if ( nCopiedFeatures == 0 ) From 104598b578e05e22cdb8080fbf6b6f0d7a679e05 Mon Sep 17 00:00:00 2001 From: David Signer Date: Tue, 7 Jan 2020 12:42:06 +0100 Subject: [PATCH 02/10] attribute dialog for fixing invalid features on paste from clipboard with the option to cancel all or store invalid anyway --- .../auto_generated/qgsattributedialog.sip.in | 2 +- src/app/qgisapp.cpp | 71 ++++++++++++------- src/gui/qgsattributedialog.cpp | 20 +++++- src/gui/qgsattributedialog.h | 4 +- 4 files changed, 67 insertions(+), 30 deletions(-) diff --git a/python/gui/auto_generated/qgsattributedialog.sip.in b/python/gui/auto_generated/qgsattributedialog.sip.in index fca8b4e59a06..8ca5df589c5a 100644 --- a/python/gui/auto_generated/qgsattributedialog.sip.in +++ b/python/gui/auto_generated/qgsattributedialog.sip.in @@ -18,7 +18,7 @@ class QgsAttributeDialog : QDialog %End public: - QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent /TransferThis/ = 0, bool showDialogButtons = true, const QgsAttributeEditorContext &context = QgsAttributeEditorContext() ); + QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent /TransferThis/ = 0, bool showDialogButtons = true, const QgsAttributeEditorContext &context = QgsAttributeEditorContext(), bool showFixFeatureDialogButtons = false ); %Docstring Create an attribute dialog for a given layer and feature diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index c3fde6643433..b64a125bc196 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -9590,41 +9590,64 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer ) // values and field constraints QgsFeatureList newFeatures {QgsVectorLayerUtils::createFeatures( pasteVectorLayer, newFeaturesDataList, &context )}; - // check against hard constraints - for ( QgsFeature &f : newFeatures ) + // check constraints + QgsFeatureList validFeatures = newFeatures; + QgsFeatureList invalidFeatures; + newFeatures.clear(); + + for ( const QgsFeature &f : qgis::as_const( validFeatures ) ) { - bool valid = true; for ( int idx = 0; idx < pasteVectorLayer->fields().count(); ++idx ) { QStringList errors; - valid = QgsVectorLayerUtils::validateAttribute( pasteVectorLayer, f, idx, errors, QgsFieldConstraints::ConstraintStrengthHard, QgsFieldConstraints::ConstraintOriginNotSet ); - if ( !valid ) + if ( !QgsVectorLayerUtils::validateAttribute( pasteVectorLayer, f, idx, errors, QgsFieldConstraints::ConstraintStrengthHard, QgsFieldConstraints::ConstraintOriginNotSet ) ) { + invalidFeatures << f; + validFeatures.removeOne( f ); break; } } + } - //open attribute form including option "cancel copy all" / "cancel copy of invalid" / "store invalid features" - if ( !valid ) - { - //QgsFeatureAction action( tr( "Fix feature" ), f, pasteVectorLayer, QString(), -1, QgisApp::instance() ); - //action.editFeature( true ); - QgsAttributeDialog *dialog = new QgsAttributeDialog( pasteVectorLayer, &f, false ); - - if ( !f.isValid() ) - dialog->setMode( QgsAttributeEditorContext::AddFeatureMode ); + for ( const QgsFeature &f : qgis::as_const( invalidFeatures ) ) + { + // open attribute form to fix invalid features and offer the options: + // - fix part of invalid features and save (without the unfixed) -> on cancel invalid or all fixed + // - fix part of invalid features and save (with the unfixed as invalid) -> on store invalid + // - cancel everything + QgsFeature feature = f; + QgsAttributeDialog *dialog = new QgsAttributeDialog( pasteVectorLayer, &feature, false, nullptr, true, QgsAttributeEditorContext(), true ); - int feedback = dialog->exec(); + int feedback = dialog->exec(); - if ( feedback > 0 ) - { - f.setAttributes( dialog->feature()->attributes() ); - } - else - { - visibleMessageBar()->pushMessage( tr( "Paste features" ), tr( "Do not copy feature" ), Qgis::Warning, messageTimeout() ); - newFeatures.removeOne( f ); - } + if ( feedback == 0 ) + { + //feature unfixed + continue; + } + else if ( feedback == 1 ) + { + //feature fixed + feature.setAttributes( dialog->feature()->attributes() ); + validFeatures << feature; + invalidFeatures.removeOne( f ); + } + else if ( feedback == 10 ) + { + //cancel all + break; + } + else if ( feedback == 11 ) + { + //cancel all invalid + newFeatures << validFeatures; + break; + } + else if ( feedback == 12 ) + { + //store all invalid + newFeatures << validFeatures << invalidFeatures; + break; } } diff --git a/src/gui/qgsattributedialog.cpp b/src/gui/qgsattributedialog.cpp index 139cf5393686..f71bb49f970c 100644 --- a/src/gui/qgsattributedialog.cpp +++ b/src/gui/qgsattributedialog.cpp @@ -21,12 +21,13 @@ #include "qgshighlight.h" #include "qgsapplication.h" #include "qgssettings.h" +#include -QgsAttributeDialog::QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent, bool showDialogButtons, const QgsAttributeEditorContext &context ) +QgsAttributeDialog::QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent, bool showDialogButtons, const QgsAttributeEditorContext &context, bool showFixFeatureDialogButtons ) : QDialog( parent ) , mOwnedFeature( featureOwner ? thepFeature : nullptr ) { - init( vl, thepFeature, context, showDialogButtons ); + init( vl, thepFeature, context, showDialogButtons, showFixFeatureDialogButtons ); } QgsAttributeDialog::~QgsAttributeDialog() @@ -85,7 +86,7 @@ void QgsAttributeDialog::reject() QDialog::reject(); } -void QgsAttributeDialog::init( QgsVectorLayer *layer, QgsFeature *feature, const QgsAttributeEditorContext &context, bool showDialogButtons ) +void QgsAttributeDialog::init( QgsVectorLayer *layer, QgsFeature *feature, const QgsAttributeEditorContext &context, bool showDialogButtons, bool showFixFeatureDialogButtons ) { QgsAttributeEditorContext trackedContext = context; setWindowTitle( tr( "%1 - Feature Attributes" ).arg( layer->name() ) ); @@ -100,6 +101,19 @@ void QgsAttributeDialog::init( QgsVectorLayer *layer, QgsFeature *feature, const mAttributeForm->disconnectButtonBox(); layout()->addWidget( mAttributeForm ); QDialogButtonBox *buttonBox = mAttributeForm->findChild(); + + if ( showFixFeatureDialogButtons ) + { + QPushButton *cancelAllBtn = new QPushButton( tr( "Cancel all" ) ); + QPushButton *cancelAllInvalidBtn = new QPushButton( tr( "Cancel all invalid" ) ); + QPushButton *storeAllInvalidBtn = new QPushButton( tr( "Store all (even invalid)" ) ); + buttonBox->addButton( cancelAllBtn, QDialogButtonBox::RejectRole ); + buttonBox->addButton( cancelAllInvalidBtn, QDialogButtonBox::RejectRole ); + buttonBox->addButton( storeAllInvalidBtn, QDialogButtonBox::RejectRole ); + connect( cancelAllBtn, &QAbstractButton::clicked, this, [ = ]() { done( 10 ); } ); + connect( cancelAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() { done( 11 ); } ); + connect( storeAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() { done( 12 ); } ); + } connect( buttonBox, &QDialogButtonBox::rejected, this, &QgsAttributeDialog::reject ); connect( buttonBox, &QDialogButtonBox::accepted, this, &QgsAttributeDialog::accept ); connect( layer, &QObject::destroyed, this, &QWidget::close ); diff --git a/src/gui/qgsattributedialog.h b/src/gui/qgsattributedialog.h index ccab920ebb1d..df12841fe388 100644 --- a/src/gui/qgsattributedialog.h +++ b/src/gui/qgsattributedialog.h @@ -51,7 +51,7 @@ class GUI_EXPORT QgsAttributeDialog : public QDialog * \param context The context in which this dialog is created * */ - QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent SIP_TRANSFERTHIS = nullptr, bool showDialogButtons = true, const QgsAttributeEditorContext &context = QgsAttributeEditorContext() ); + QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent SIP_TRANSFERTHIS = nullptr, bool showDialogButtons = true, const QgsAttributeEditorContext &context = QgsAttributeEditorContext(), bool showFixFeatureDialogButtons = false ); ~QgsAttributeDialog() override; @@ -103,7 +103,7 @@ class GUI_EXPORT QgsAttributeDialog : public QDialog void show(); private: - void init( QgsVectorLayer *layer, QgsFeature *feature, const QgsAttributeEditorContext &context, bool showDialogButtons ); + void init( QgsVectorLayer *layer, QgsFeature *feature, const QgsAttributeEditorContext &context, bool showDialogButtons, bool showFixFeatureDialogButtons ); QString mSettingsPath; // Used to sync multiple widgets for the same field From c4dc133ab65cc168e145e17dc2314a0c0637be12 Mon Sep 17 00:00:00 2001 From: David Signer Date: Tue, 7 Jan 2020 16:13:30 +0100 Subject: [PATCH 03/10] handle list of features to attributedialog on feature fix process --- .../auto_generated/qgsattributedialog.sip.in | 12 ++ src/app/qgisapp.cpp | 59 ++++----- src/gui/qgsattributedialog.cpp | 112 +++++++++++++++--- src/gui/qgsattributedialog.h | 19 ++- 4 files changed, 148 insertions(+), 54 deletions(-) diff --git a/python/gui/auto_generated/qgsattributedialog.sip.in b/python/gui/auto_generated/qgsattributedialog.sip.in index 8ca5df589c5a..eaad71e0563f 100644 --- a/python/gui/auto_generated/qgsattributedialog.sip.in +++ b/python/gui/auto_generated/qgsattributedialog.sip.in @@ -28,6 +28,16 @@ Create an attribute dialog for a given layer and feature :param parent: A parent widget for the dialog :param showDialogButtons: ``True``: Show the dialog buttons accept/cancel :param context: The context in which this dialog is created +%End + + QgsAttributeDialog( QgsVectorLayer *vl, QgsFeatureList *features, QWidget *parent /TransferThis/ = 0, const QgsAttributeEditorContext &context = QgsAttributeEditorContext() ); +%Docstring +Create an attribute dialog for a given layer and a feature list + +:param vl: The layer for which the dialog will be generated +:param features: A list of features handled in the dialog +:param parent: A parent widget for the dialog +:param context: The context in which this dialog is created %End ~QgsAttributeDialog(); @@ -75,6 +85,8 @@ Intercept window activate/deactivate events to show/hide the highlighted feature :return: The same as the parent QDialog %End + QgsFeatureList validFeatures(); + public slots: virtual void accept(); diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index b64a125bc196..7ce285a60347 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -9609,46 +9609,27 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer ) } } - for ( const QgsFeature &f : qgis::as_const( invalidFeatures ) ) - { - // open attribute form to fix invalid features and offer the options: - // - fix part of invalid features and save (without the unfixed) -> on cancel invalid or all fixed - // - fix part of invalid features and save (with the unfixed as invalid) -> on store invalid - // - cancel everything - QgsFeature feature = f; - QgsAttributeDialog *dialog = new QgsAttributeDialog( pasteVectorLayer, &feature, false, nullptr, true, QgsAttributeEditorContext(), true ); - - int feedback = dialog->exec(); + // open attribute form to fix invalid features and offer the options: + // - fix part of invalid features and save (without the unfixed) -> on cancel invalid or all fixed + // - fix part of invalid features and save (with the unfixed as invalid) -> on store invalid + // - cancel everything + QgsAttributeDialog *dialog = new QgsAttributeDialog( pasteVectorLayer, &invalidFeatures, nullptr, QgsAttributeEditorContext() ); + dialog->setMode( QgsAttributeEditorContext::AddFeatureMode ); + int feedback = dialog->exec(); - if ( feedback == 0 ) - { - //feature unfixed - continue; - } - else if ( feedback == 1 ) - { - //feature fixed - feature.setAttributes( dialog->feature()->attributes() ); - validFeatures << feature; - invalidFeatures.removeOne( f ); - } - else if ( feedback == 10 ) - { - //cancel all - break; - } - else if ( feedback == 11 ) - { - //cancel all invalid - newFeatures << validFeatures; - break; - } - else if ( feedback == 12 ) - { - //store all invalid - newFeatures << validFeatures << invalidFeatures; - break; - } + if ( feedback == 10 ) + { + //cancel all + } + else if ( feedback == 11 ) + { + //cancel all invalid + newFeatures << validFeatures << dialog->validFeatures(); + } + else if ( feedback == 12 ) + { + //store all invalid + newFeatures << validFeatures << invalidFeatures; } pasteVectorLayer->addFeatures( newFeatures ); diff --git a/src/gui/qgsattributedialog.cpp b/src/gui/qgsattributedialog.cpp index f71bb49f970c..8ef807fcda02 100644 --- a/src/gui/qgsattributedialog.cpp +++ b/src/gui/qgsattributedialog.cpp @@ -21,13 +21,21 @@ #include "qgshighlight.h" #include "qgsapplication.h" #include "qgssettings.h" + +#include #include QgsAttributeDialog::QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent, bool showDialogButtons, const QgsAttributeEditorContext &context, bool showFixFeatureDialogButtons ) : QDialog( parent ) , mOwnedFeature( featureOwner ? thepFeature : nullptr ) { - init( vl, thepFeature, context, showDialogButtons, showFixFeatureDialogButtons ); + init( vl, thepFeature, context, showDialogButtons ); +} + +QgsAttributeDialog::QgsAttributeDialog( QgsVectorLayer *vl, QgsFeatureList *features, QWidget *parent, const QgsAttributeEditorContext &context ) + : QDialog( parent ) +{ + initList( vl, features, context ); } QgsAttributeDialog::~QgsAttributeDialog() @@ -86,7 +94,7 @@ void QgsAttributeDialog::reject() QDialog::reject(); } -void QgsAttributeDialog::init( QgsVectorLayer *layer, QgsFeature *feature, const QgsAttributeEditorContext &context, bool showDialogButtons, bool showFixFeatureDialogButtons ) +void QgsAttributeDialog::init( QgsVectorLayer *layer, QgsFeature *feature, const QgsAttributeEditorContext &context, bool showDialogButtons ) { QgsAttributeEditorContext trackedContext = context; setWindowTitle( tr( "%1 - Feature Attributes" ).arg( layer->name() ) ); @@ -101,21 +109,97 @@ void QgsAttributeDialog::init( QgsVectorLayer *layer, QgsFeature *feature, const mAttributeForm->disconnectButtonBox(); layout()->addWidget( mAttributeForm ); QDialogButtonBox *buttonBox = mAttributeForm->findChild(); + connect( buttonBox, &QDialogButtonBox::rejected, this, &QgsAttributeDialog::reject ); + connect( buttonBox, &QDialogButtonBox::accepted, this, &QgsAttributeDialog::accept ); + connect( layer, &QObject::destroyed, this, &QWidget::close ); - if ( showFixFeatureDialogButtons ) + mMenu = new QgsActionMenu( layer, mAttributeForm->feature(), QStringLiteral( "Feature" ), this ); + if ( !mMenu->actions().isEmpty() ) { - QPushButton *cancelAllBtn = new QPushButton( tr( "Cancel all" ) ); - QPushButton *cancelAllInvalidBtn = new QPushButton( tr( "Cancel all invalid" ) ); - QPushButton *storeAllInvalidBtn = new QPushButton( tr( "Store all (even invalid)" ) ); - buttonBox->addButton( cancelAllBtn, QDialogButtonBox::RejectRole ); - buttonBox->addButton( cancelAllInvalidBtn, QDialogButtonBox::RejectRole ); - buttonBox->addButton( storeAllInvalidBtn, QDialogButtonBox::RejectRole ); - connect( cancelAllBtn, &QAbstractButton::clicked, this, [ = ]() { done( 10 ); } ); - connect( cancelAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() { done( 11 ); } ); - connect( storeAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() { done( 12 ); } ); + QMenuBar *menuBar = new QMenuBar( this ); + menuBar->addMenu( mMenu ); + layout()->setMenuBar( menuBar ); } - connect( buttonBox, &QDialogButtonBox::rejected, this, &QgsAttributeDialog::reject ); - connect( buttonBox, &QDialogButtonBox::accepted, this, &QgsAttributeDialog::accept ); + + restoreGeometry(); + focusNextChild(); +} + +void QgsAttributeDialog::initList( QgsVectorLayer *layer, QgsFeatureList *features, const QgsAttributeEditorContext &context ) +{ + + QgsAttributeEditorContext trackedContext = context; + setWindowTitle( tr( "%1 - Fix Feature Attributes" ).arg( layer->name() ) ); + setLayout( new QGridLayout() ); + layout()->setMargin( 0 ); + mTrackedVectorLayerTools.setVectorLayerTools( trackedContext.vectorLayerTools() ); + trackedContext.setVectorLayerTools( &mTrackedVectorLayerTools ); + trackedContext.setFormMode( QgsAttributeEditorContext::StandaloneDialog ); + + mCurrentIndex = 0; + QLabel *description = new QLabel( tr( "%1 features do not fullfill the constraints. Please fix or cancel them." ).arg( features->count() ) ); + layout()->addWidget( description ); + QProgressBar *progressBar = new QProgressBar(); + progressBar->setOrientation( Qt::Horizontal ); + progressBar->setRange( 0, features->count() ); + + layout()->addWidget( progressBar ); + mAttributeForm = new QgsAttributeForm( layer, features->at( mCurrentIndex ), trackedContext, this ); + mAttributeForm->disconnectButtonBox(); + layout()->addWidget( mAttributeForm ); + QDialogButtonBox *buttonBox = mAttributeForm->findChild(); + + QPushButton *cancelAllBtn = new QPushButton( tr( "Cancel all" ) ); + QPushButton *cancelAllInvalidBtn = new QPushButton( tr( "Cancel all invalid" ) ); + QPushButton *storeAllInvalidBtn = new QPushButton( tr( "Store all (even invalid)" ) ); + buttonBox->addButton( cancelAllBtn, QDialogButtonBox::RejectRole ); + buttonBox->addButton( cancelAllInvalidBtn, QDialogButtonBox::RejectRole ); + buttonBox->addButton( storeAllInvalidBtn, QDialogButtonBox::RejectRole ); + connect( cancelAllBtn, &QAbstractButton::clicked, this, [ = ]() + { + done( 10 ); + } ); + connect( cancelAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() + { + done( 11 ); + } ); + connect( storeAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() + { + done( 12 ); + } ); + connect( buttonBox, &QDialogButtonBox::rejected, this, [ = ]() + { + //next feature + mCurrentIndex++; + progressBar->setValue( mCurrentIndex ); + if ( mCurrentIndex < features->count() ) + { + mAttributeForm->setFeature( features->at( mCurrentIndex ) ); + } + else + { + done( 11 ); + } + } ); + connect( buttonBox, &QDialogButtonBox::accepted, this, [ = ]() + { + mAttributeForm->save(); + QgsFeature feature; + feature.setAttributes( mAttributeForm->feature().attributes() ); + mValidFeatures << feature; + //next feature + mCurrentIndex++; + progressBar->setValue( mCurrentIndex ); + if ( mCurrentIndex < features->count() ) + { + mAttributeForm->setFeature( features->at( mCurrentIndex ) ); + } + else + { + done( 11 ); + } + } ); + connect( layer, &QObject::destroyed, this, &QWidget::close ); mMenu = new QgsActionMenu( layer, mAttributeForm->feature(), QStringLiteral( "Feature" ), this ); diff --git a/src/gui/qgsattributedialog.h b/src/gui/qgsattributedialog.h index df12841fe388..7b2058562b9e 100644 --- a/src/gui/qgsattributedialog.h +++ b/src/gui/qgsattributedialog.h @@ -53,6 +53,17 @@ class GUI_EXPORT QgsAttributeDialog : public QDialog */ QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent SIP_TRANSFERTHIS = nullptr, bool showDialogButtons = true, const QgsAttributeEditorContext &context = QgsAttributeEditorContext(), bool showFixFeatureDialogButtons = false ); + /** + * Create an attribute dialog for a given layer and a feature list + * + * \param vl The layer for which the dialog will be generated + * \param features A list of features handled in the dialog + * \param parent A parent widget for the dialog + * \param context The context in which this dialog is created + * + */ + QgsAttributeDialog( QgsVectorLayer *vl, QgsFeatureList *features, QWidget *parent SIP_TRANSFERTHIS = nullptr, const QgsAttributeEditorContext &context = QgsAttributeEditorContext() ); + ~QgsAttributeDialog() override; /** @@ -95,6 +106,8 @@ class GUI_EXPORT QgsAttributeDialog : public QDialog */ bool event( QEvent *e ) override; + QgsFeatureList validFeatures() { return mValidFeatures; } + public slots: void accept() override; void reject() override; @@ -103,7 +116,8 @@ class GUI_EXPORT QgsAttributeDialog : public QDialog void show(); private: - void init( QgsVectorLayer *layer, QgsFeature *feature, const QgsAttributeEditorContext &context, bool showDialogButtons, bool showFixFeatureDialogButtons ); + void init( QgsVectorLayer *layer, QgsFeature *feature, const QgsAttributeEditorContext &context, bool showDialogButtons ); + void initList( QgsVectorLayer *layer, QgsFeatureList *features, const QgsAttributeEditorContext &context ); QString mSettingsPath; // Used to sync multiple widgets for the same field @@ -121,6 +135,9 @@ class GUI_EXPORT QgsAttributeDialog : public QDialog QgsActionMenu *mMenu; + int mCurrentIndex = 0; + QgsFeatureList mValidFeatures; + static int sFormCounter; void saveGeometry(); From f5e01d3c80b4d281243d41b94cb6fb4ee8fdd464 Mon Sep 17 00:00:00 2001 From: David Signer Date: Thu, 9 Jan 2020 09:55:20 +0100 Subject: [PATCH 04/10] QgsFixAttributeDialog seperated from QgsAttributeDialog to have the list of features that are not stored yet there for fixing the attributes --- .../auto_generated/qgsattributedialog.sip.in | 14 +-- src/app/qgisapp.cpp | 44 ++++--- src/gui/CMakeLists.txt | 3 + src/gui/qgsattributedialog.cpp | 100 +--------------- src/gui/qgsattributedialog.h | 19 +-- src/gui/qgsfixattributedialog.cpp | 110 ++++++++++++++++++ src/gui/qgsfixattributedialog.h | 66 +++++++++++ 7 files changed, 203 insertions(+), 153 deletions(-) create mode 100644 src/gui/qgsfixattributedialog.cpp create mode 100644 src/gui/qgsfixattributedialog.h diff --git a/python/gui/auto_generated/qgsattributedialog.sip.in b/python/gui/auto_generated/qgsattributedialog.sip.in index eaad71e0563f..fca8b4e59a06 100644 --- a/python/gui/auto_generated/qgsattributedialog.sip.in +++ b/python/gui/auto_generated/qgsattributedialog.sip.in @@ -18,7 +18,7 @@ class QgsAttributeDialog : QDialog %End public: - QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent /TransferThis/ = 0, bool showDialogButtons = true, const QgsAttributeEditorContext &context = QgsAttributeEditorContext(), bool showFixFeatureDialogButtons = false ); + QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent /TransferThis/ = 0, bool showDialogButtons = true, const QgsAttributeEditorContext &context = QgsAttributeEditorContext() ); %Docstring Create an attribute dialog for a given layer and feature @@ -28,16 +28,6 @@ Create an attribute dialog for a given layer and feature :param parent: A parent widget for the dialog :param showDialogButtons: ``True``: Show the dialog buttons accept/cancel :param context: The context in which this dialog is created -%End - - QgsAttributeDialog( QgsVectorLayer *vl, QgsFeatureList *features, QWidget *parent /TransferThis/ = 0, const QgsAttributeEditorContext &context = QgsAttributeEditorContext() ); -%Docstring -Create an attribute dialog for a given layer and a feature list - -:param vl: The layer for which the dialog will be generated -:param features: A list of features handled in the dialog -:param parent: A parent widget for the dialog -:param context: The context in which this dialog is created %End ~QgsAttributeDialog(); @@ -85,8 +75,6 @@ Intercept window activate/deactivate events to show/hide the highlighted feature :return: The same as the parent QDialog %End - QgsFeatureList validFeatures(); - public slots: virtual void accept(); diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 7ce285a60347..10e183a63ea2 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -87,7 +87,7 @@ #include "qgssourceselectproviderregistry.h" #include "qgssourceselectprovider.h" #include "qgsprovidermetadata.h" -#include "qgsattributedialog.h" +#include "qgsfixattributedialog.h" #include "qgsanalysis.h" #include "qgsgeometrycheckregistry.h" @@ -9593,8 +9593,6 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer ) // check constraints QgsFeatureList validFeatures = newFeatures; QgsFeatureList invalidFeatures; - newFeatures.clear(); - for ( const QgsFeature &f : qgis::as_const( validFeatures ) ) { for ( int idx = 0; idx < pasteVectorLayer->fields().count(); ++idx ) @@ -9609,27 +9607,27 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer ) } } - // open attribute form to fix invalid features and offer the options: - // - fix part of invalid features and save (without the unfixed) -> on cancel invalid or all fixed - // - fix part of invalid features and save (with the unfixed as invalid) -> on store invalid - // - cancel everything - QgsAttributeDialog *dialog = new QgsAttributeDialog( pasteVectorLayer, &invalidFeatures, nullptr, QgsAttributeEditorContext() ); - dialog->setMode( QgsAttributeEditorContext::AddFeatureMode ); - int feedback = dialog->exec(); - - if ( feedback == 10 ) - { - //cancel all - } - else if ( feedback == 11 ) - { - //cancel all invalid - newFeatures << validFeatures << dialog->validFeatures(); - } - else if ( feedback == 12 ) + if ( !invalidFeatures.isEmpty() ) { - //store all invalid - newFeatures << validFeatures << invalidFeatures; + newFeatures.clear(); + + QgsFixAttributeDialog *dialog = new QgsFixAttributeDialog( pasteVectorLayer, invalidFeatures, this ); + int feedback = dialog->exec(); + + if ( feedback == QgsFixAttributeDialog::VanishAll ) + { + //vanish all + } + else if ( feedback == QgsFixAttributeDialog::CopyValid ) + { + //copy valid and fixed, vanish unfixed + newFeatures << validFeatures << dialog->fixedFeatures(); + } + else if ( feedback == QgsFixAttributeDialog::CopyAll ) + { + //copy all, even unfixed + newFeatures << validFeatures << dialog->fixedFeatures() << dialog->unfixedFeatures(); + } } pasteVectorLayer->addFeatures( newFeatures ); diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 03cc6742d834..cd198bdde624 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -300,6 +300,7 @@ SET(QGIS_GUI_SRCS qgsfeatureselectiondlg.cpp qgsfieldcombobox.cpp qgsfieldexpressionwidget.cpp + qgsfixattributedialog.cpp qgsfeaturelistcombobox.cpp qgsfieldvalidator.cpp qgsfieldvalueslineedit.cpp @@ -307,6 +308,7 @@ SET(QGIS_GUI_SRCS qgsfilewidget.cpp qgsfilterlineedit.cpp qgsfindfilesbypatternwidget.cpp + qgsfixattributedialog.cpp qgsfloatingwidget.cpp qgsfocuswatcher.cpp qgsfontbutton.cpp @@ -511,6 +513,7 @@ SET(QGIS_GUI_HDRS qgsfilewidget.h qgsfilterlineedit.h qgsfindfilesbypatternwidget.h + qgsfixattributedialog.h qgsfloatingwidget.h qgsfocuswatcher.h qgsfontbutton.h diff --git a/src/gui/qgsattributedialog.cpp b/src/gui/qgsattributedialog.cpp index 8ef807fcda02..139cf5393686 100644 --- a/src/gui/qgsattributedialog.cpp +++ b/src/gui/qgsattributedialog.cpp @@ -22,22 +22,13 @@ #include "qgsapplication.h" #include "qgssettings.h" -#include -#include - -QgsAttributeDialog::QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent, bool showDialogButtons, const QgsAttributeEditorContext &context, bool showFixFeatureDialogButtons ) +QgsAttributeDialog::QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent, bool showDialogButtons, const QgsAttributeEditorContext &context ) : QDialog( parent ) , mOwnedFeature( featureOwner ? thepFeature : nullptr ) { init( vl, thepFeature, context, showDialogButtons ); } -QgsAttributeDialog::QgsAttributeDialog( QgsVectorLayer *vl, QgsFeatureList *features, QWidget *parent, const QgsAttributeEditorContext &context ) - : QDialog( parent ) -{ - initList( vl, features, context ); -} - QgsAttributeDialog::~QgsAttributeDialog() { if ( mHighlight ) @@ -125,95 +116,6 @@ void QgsAttributeDialog::init( QgsVectorLayer *layer, QgsFeature *feature, const focusNextChild(); } -void QgsAttributeDialog::initList( QgsVectorLayer *layer, QgsFeatureList *features, const QgsAttributeEditorContext &context ) -{ - - QgsAttributeEditorContext trackedContext = context; - setWindowTitle( tr( "%1 - Fix Feature Attributes" ).arg( layer->name() ) ); - setLayout( new QGridLayout() ); - layout()->setMargin( 0 ); - mTrackedVectorLayerTools.setVectorLayerTools( trackedContext.vectorLayerTools() ); - trackedContext.setVectorLayerTools( &mTrackedVectorLayerTools ); - trackedContext.setFormMode( QgsAttributeEditorContext::StandaloneDialog ); - - mCurrentIndex = 0; - QLabel *description = new QLabel( tr( "%1 features do not fullfill the constraints. Please fix or cancel them." ).arg( features->count() ) ); - layout()->addWidget( description ); - QProgressBar *progressBar = new QProgressBar(); - progressBar->setOrientation( Qt::Horizontal ); - progressBar->setRange( 0, features->count() ); - - layout()->addWidget( progressBar ); - mAttributeForm = new QgsAttributeForm( layer, features->at( mCurrentIndex ), trackedContext, this ); - mAttributeForm->disconnectButtonBox(); - layout()->addWidget( mAttributeForm ); - QDialogButtonBox *buttonBox = mAttributeForm->findChild(); - - QPushButton *cancelAllBtn = new QPushButton( tr( "Cancel all" ) ); - QPushButton *cancelAllInvalidBtn = new QPushButton( tr( "Cancel all invalid" ) ); - QPushButton *storeAllInvalidBtn = new QPushButton( tr( "Store all (even invalid)" ) ); - buttonBox->addButton( cancelAllBtn, QDialogButtonBox::RejectRole ); - buttonBox->addButton( cancelAllInvalidBtn, QDialogButtonBox::RejectRole ); - buttonBox->addButton( storeAllInvalidBtn, QDialogButtonBox::RejectRole ); - connect( cancelAllBtn, &QAbstractButton::clicked, this, [ = ]() - { - done( 10 ); - } ); - connect( cancelAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() - { - done( 11 ); - } ); - connect( storeAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() - { - done( 12 ); - } ); - connect( buttonBox, &QDialogButtonBox::rejected, this, [ = ]() - { - //next feature - mCurrentIndex++; - progressBar->setValue( mCurrentIndex ); - if ( mCurrentIndex < features->count() ) - { - mAttributeForm->setFeature( features->at( mCurrentIndex ) ); - } - else - { - done( 11 ); - } - } ); - connect( buttonBox, &QDialogButtonBox::accepted, this, [ = ]() - { - mAttributeForm->save(); - QgsFeature feature; - feature.setAttributes( mAttributeForm->feature().attributes() ); - mValidFeatures << feature; - //next feature - mCurrentIndex++; - progressBar->setValue( mCurrentIndex ); - if ( mCurrentIndex < features->count() ) - { - mAttributeForm->setFeature( features->at( mCurrentIndex ) ); - } - else - { - done( 11 ); - } - } ); - - connect( layer, &QObject::destroyed, this, &QWidget::close ); - - mMenu = new QgsActionMenu( layer, mAttributeForm->feature(), QStringLiteral( "Feature" ), this ); - if ( !mMenu->actions().isEmpty() ) - { - QMenuBar *menuBar = new QMenuBar( this ); - menuBar->addMenu( mMenu ); - layout()->setMenuBar( menuBar ); - } - - restoreGeometry(); - focusNextChild(); -} - void QgsAttributeDialog::setMode( QgsAttributeEditorContext::Mode mode ) { mAttributeForm->setMode( mode ); diff --git a/src/gui/qgsattributedialog.h b/src/gui/qgsattributedialog.h index 7b2058562b9e..ccab920ebb1d 100644 --- a/src/gui/qgsattributedialog.h +++ b/src/gui/qgsattributedialog.h @@ -51,18 +51,7 @@ class GUI_EXPORT QgsAttributeDialog : public QDialog * \param context The context in which this dialog is created * */ - QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent SIP_TRANSFERTHIS = nullptr, bool showDialogButtons = true, const QgsAttributeEditorContext &context = QgsAttributeEditorContext(), bool showFixFeatureDialogButtons = false ); - - /** - * Create an attribute dialog for a given layer and a feature list - * - * \param vl The layer for which the dialog will be generated - * \param features A list of features handled in the dialog - * \param parent A parent widget for the dialog - * \param context The context in which this dialog is created - * - */ - QgsAttributeDialog( QgsVectorLayer *vl, QgsFeatureList *features, QWidget *parent SIP_TRANSFERTHIS = nullptr, const QgsAttributeEditorContext &context = QgsAttributeEditorContext() ); + QgsAttributeDialog( QgsVectorLayer *vl, QgsFeature *thepFeature, bool featureOwner, QWidget *parent SIP_TRANSFERTHIS = nullptr, bool showDialogButtons = true, const QgsAttributeEditorContext &context = QgsAttributeEditorContext() ); ~QgsAttributeDialog() override; @@ -106,8 +95,6 @@ class GUI_EXPORT QgsAttributeDialog : public QDialog */ bool event( QEvent *e ) override; - QgsFeatureList validFeatures() { return mValidFeatures; } - public slots: void accept() override; void reject() override; @@ -117,7 +104,6 @@ class GUI_EXPORT QgsAttributeDialog : public QDialog private: void init( QgsVectorLayer *layer, QgsFeature *feature, const QgsAttributeEditorContext &context, bool showDialogButtons ); - void initList( QgsVectorLayer *layer, QgsFeatureList *features, const QgsAttributeEditorContext &context ); QString mSettingsPath; // Used to sync multiple widgets for the same field @@ -135,9 +121,6 @@ class GUI_EXPORT QgsAttributeDialog : public QDialog QgsActionMenu *mMenu; - int mCurrentIndex = 0; - QgsFeatureList mValidFeatures; - static int sFormCounter; void saveGeometry(); diff --git a/src/gui/qgsfixattributedialog.cpp b/src/gui/qgsfixattributedialog.cpp new file mode 100644 index 000000000000..48a10d49f755 --- /dev/null +++ b/src/gui/qgsfixattributedialog.cpp @@ -0,0 +1,110 @@ +#include "qgsfixattributedialog.h" + +#include "qgsattributeform.h" +#include "qgsapplication.h" + +#include + +QgsFixAttributeDialog::QgsFixAttributeDialog( QgsVectorLayer *vl, QgsFeatureList &features, QWidget *parent ) + : QDialog( parent ) + , mFeatures( features ) +{ + init( vl ); +} + +void QgsFixAttributeDialog::init( QgsVectorLayer *layer ) +{ + QgsAttributeEditorContext context; + setWindowTitle( tr( "%1 - Fix Feature Attributes" ).arg( layer->name() ) ); + setLayout( new QGridLayout() ); + layout()->setMargin( 0 ); + context.setFormMode( QgsAttributeEditorContext::StandaloneDialog ); + + mUnfixedFeatures = mFeatures; + currentFeature = mFeatures.begin(); + + QGridLayout *infoLayout = new QGridLayout(); + QWidget *infoBox = new QWidget(); + infoBox->setLayout( infoLayout ); + layout()->addWidget( infoBox ); + + mDescription = new QLabel( descriptionText() ); + infoLayout->addWidget( mDescription ); + mProgressBar = new QProgressBar(); + mProgressBar->setOrientation( Qt::Horizontal ); + mProgressBar->setRange( 0, mFeatures.count() ); + infoLayout->addWidget( mProgressBar ); + QgsFeature feature; + mAttributeForm = new QgsAttributeForm( layer, *currentFeature, context, this ); + mAttributeForm->setMode( QgsAttributeEditorContext::SingleEditMode ); + mAttributeForm->disconnectButtonBox(); + layout()->addWidget( mAttributeForm ); + + QDialogButtonBox *buttonBox = mAttributeForm->findChild(); + QPushButton *cancelAllBtn = new QPushButton( tr( "Cancel all" ) ); + QPushButton *cancelAllInvalidBtn = new QPushButton( tr( "Cancel all invalid" ) ); + QPushButton *storeAllInvalidBtn = new QPushButton( tr( "Store all (even invalid)" ) ); + buttonBox->addButton( cancelAllBtn, QDialogButtonBox::ActionRole ); + buttonBox->addButton( cancelAllInvalidBtn, QDialogButtonBox::ActionRole ); + buttonBox->addButton( storeAllInvalidBtn, QDialogButtonBox::ActionRole ); + connect( cancelAllBtn, &QAbstractButton::clicked, this, [ = ]() + { + done( VanishAll ); + } ); + connect( cancelAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() + { + done( CopyValid ); + } ); + connect( storeAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() + { + done( CopyAll ); + } ); + connect( buttonBox, &QDialogButtonBox::rejected, this, &QgsFixAttributeDialog::reject ); + connect( buttonBox, &QDialogButtonBox::accepted, this, &QgsFixAttributeDialog::accept ); + connect( layer, &QObject::destroyed, this, &QWidget::close ); + + focusNextChild(); +} + +QString QgsFixAttributeDialog::descriptionText() +{ + return tr( "%1 of %2 features fixed\n%3 of %4 features canceled" ).arg( fixedFeatures().count() ).arg( mFeatures.count() ).arg( currentFeature - mFeatures.begin() - fixedFeatures().count() ).arg( mFeatures.count() ); +} + +void QgsFixAttributeDialog::accept() +{ + mAttributeForm->save(); + mFixedFeatures << mAttributeForm->feature(); + mUnfixedFeatures.removeOne( *currentFeature ); + + //next feature + ++currentFeature; + if ( currentFeature != mFeatures.end() ) + { + mAttributeForm->setFeature( *currentFeature ); + } + else + { + done( CopyValid ); + } + + mProgressBar->setValue( currentFeature - mFeatures.begin() ); + mDescription->setText( descriptionText() ); +} + +void QgsFixAttributeDialog::reject() +{ + //next feature + ++currentFeature; + if ( currentFeature != mFeatures.end() ) + { + mAttributeForm->setFeature( *currentFeature ); + } + else + { + done( CopyValid ); + } + + mProgressBar->setValue( currentFeature - mFeatures.begin() ); + mDescription->setText( descriptionText() ); +} diff --git a/src/gui/qgsfixattributedialog.h b/src/gui/qgsfixattributedialog.h new file mode 100644 index 000000000000..d6f1ad8629ff --- /dev/null +++ b/src/gui/qgsfixattributedialog.h @@ -0,0 +1,66 @@ +#ifndef QGSFIXATTRIBUTEDIALOG_H +#define QGSFIXATTRIBUTEDIALOG_H + +#include "qgsattributeeditorcontext.h" +#include "qgis_sip.h" +#include "qgsattributeform.h" +#include "qgstrackedvectorlayertools.h" + +#include +#include +#include +#include "qgis_gui.h" + +/** +* \ingroup gui +* \class QgsFixAttributeDialog +* \since 3.12 +*/ +class GUI_EXPORT QgsFixAttributeDialog : public QDialog +{ + Q_OBJECT + + public: + + enum Feedback + { + VanishAll, + CopyValid, + CopyAll + }; + + QgsFixAttributeDialog( QgsVectorLayer *vl, QgsFeatureList &features, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + + /* + * returns fixed features + */ + QgsFeatureList fixedFeatures() { return mFixedFeatures; } + + /* + * returns unfixed features + */ + QgsFeatureList unfixedFeatures() { return mUnfixedFeatures; } + + public slots: + void accept() override; + void reject() override; + + private: + void init( QgsVectorLayer *layer ); + QString descriptionText(); + + QgsFeatureList mFeatures; + QgsFeatureList::iterator currentFeature; + + //the fixed features + QgsFeatureList mFixedFeatures; + //the not yet fixed and the canceled features + QgsFeatureList mUnfixedFeatures; + + QgsAttributeForm *mAttributeForm = nullptr; + QProgressBar *mProgressBar = nullptr; + QLabel *mDescription = nullptr; +}; + +#endif // QGSFIXATTRIBUTEDIALOG_H + From 7bdfc6d66a4319feeaa414cc152c7714b75f3d00 Mon Sep 17 00:00:00 2001 From: David Signer Date: Thu, 9 Jan 2020 16:09:51 +0100 Subject: [PATCH 05/10] better naming and docs --- src/gui/CMakeLists.txt | 1 - src/gui/qgsfixattributedialog.cpp | 38 ++++++++++++++++-------- src/gui/qgsfixattributedialog.h | 49 ++++++++++++++++++++++--------- 3 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index cd198bdde624..87cfd3d20bb6 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -308,7 +308,6 @@ SET(QGIS_GUI_SRCS qgsfilewidget.cpp qgsfilterlineedit.cpp qgsfindfilesbypatternwidget.cpp - qgsfixattributedialog.cpp qgsfloatingwidget.cpp qgsfocuswatcher.cpp qgsfontbutton.cpp diff --git a/src/gui/qgsfixattributedialog.cpp b/src/gui/qgsfixattributedialog.cpp index 48a10d49f755..92d18192bc98 100644 --- a/src/gui/qgsfixattributedialog.cpp +++ b/src/gui/qgsfixattributedialog.cpp @@ -1,3 +1,17 @@ +/*************************************************************************** + qgsfixattributedialog.cpp + --------------------- + begin : January 2020 + copyright : (C) 2020 by David Signer + email : david at opengis dot ch + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ #include "qgsfixattributedialog.h" #include "qgsattributeform.h" @@ -21,7 +35,7 @@ void QgsFixAttributeDialog::init( QgsVectorLayer *layer ) context.setFormMode( QgsAttributeEditorContext::StandaloneDialog ); mUnfixedFeatures = mFeatures; - currentFeature = mFeatures.begin(); + mCurrentFeature = mFeatures.begin(); QGridLayout *infoLayout = new QGridLayout(); QWidget *infoBox = new QWidget(); @@ -35,7 +49,7 @@ void QgsFixAttributeDialog::init( QgsVectorLayer *layer ) mProgressBar->setRange( 0, mFeatures.count() ); infoLayout->addWidget( mProgressBar ); QgsFeature feature; - mAttributeForm = new QgsAttributeForm( layer, *currentFeature, context, this ); + mAttributeForm = new QgsAttributeForm( layer, *mCurrentFeature, context, this ); mAttributeForm->setMode( QgsAttributeEditorContext::SingleEditMode ); mAttributeForm->disconnectButtonBox(); layout()->addWidget( mAttributeForm ); @@ -68,43 +82,43 @@ void QgsFixAttributeDialog::init( QgsVectorLayer *layer ) QString QgsFixAttributeDialog::descriptionText() { - return tr( "%1 of %2 features fixed\n%3 of %4 features canceled" ).arg( fixedFeatures().count() ).arg( mFeatures.count() ).arg( currentFeature - mFeatures.begin() - fixedFeatures().count() ).arg( mFeatures.count() ); + return tr( "%1 of %2 features fixed\n%3 of %4 features canceled" ).arg( fixedFeatures().count() ).arg( mFeatures.count() ).arg( mCurrentFeature - mFeatures.begin() - fixedFeatures().count() ).arg( mFeatures.count() ); } void QgsFixAttributeDialog::accept() { mAttributeForm->save(); mFixedFeatures << mAttributeForm->feature(); - mUnfixedFeatures.removeOne( *currentFeature ); + mUnfixedFeatures.removeOne( *mCurrentFeature ); //next feature - ++currentFeature; - if ( currentFeature != mFeatures.end() ) + ++mCurrentFeature; + if ( mCurrentFeature != mFeatures.end() ) { - mAttributeForm->setFeature( *currentFeature ); + mAttributeForm->setFeature( *mCurrentFeature ); } else { done( CopyValid ); } - mProgressBar->setValue( currentFeature - mFeatures.begin() ); + mProgressBar->setValue( mCurrentFeature - mFeatures.begin() ); mDescription->setText( descriptionText() ); } void QgsFixAttributeDialog::reject() { //next feature - ++currentFeature; - if ( currentFeature != mFeatures.end() ) + ++mCurrentFeature; + if ( mCurrentFeature != mFeatures.end() ) { - mAttributeForm->setFeature( *currentFeature ); + mAttributeForm->setFeature( *mCurrentFeature ); } else { done( CopyValid ); } - mProgressBar->setValue( currentFeature - mFeatures.begin() ); + mProgressBar->setValue( mCurrentFeature - mFeatures.begin() ); mDescription->setText( descriptionText() ); } diff --git a/src/gui/qgsfixattributedialog.h b/src/gui/qgsfixattributedialog.h index d6f1ad8629ff..334af29eb2d0 100644 --- a/src/gui/qgsfixattributedialog.h +++ b/src/gui/qgsfixattributedialog.h @@ -1,3 +1,18 @@ +/*************************************************************************** + qgsfixattributedialog.h + --------------------- + begin : January 2020 + copyright : (C) 2020 by David Signer + email : david at opengis dot ch + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + #ifndef QGSFIXATTRIBUTEDIALOG_H #define QGSFIXATTRIBUTEDIALOG_H @@ -12,32 +27,40 @@ #include "qgis_gui.h" /** -* \ingroup gui -* \class QgsFixAttributeDialog -* \since 3.12 -*/ + * \ingroup gui + * \class QgsFixAttributeDialog + * \brief Dialog to fix a list of invalid feature regarding constraints + * \since 3.12 + */ + class GUI_EXPORT QgsFixAttributeDialog : public QDialog { Q_OBJECT public: + /** + * Feedback code on closing the dialog + */ enum Feedback { - VanishAll, - CopyValid, - CopyAll + VanishAll, //!< Feedback to cancel copy of all features (even valid ones) + CopyValid, //!< Feedback to copy the valid features and vanishe the invalid ones + CopyAll //!< Feedback to copy all features, no matter if valid or invalid }; + /** + * Constructor for QgsFixAttributeDialog + */ QgsFixAttributeDialog( QgsVectorLayer *vl, QgsFeatureList &features, QWidget *parent SIP_TRANSFERTHIS = nullptr ); - /* - * returns fixed features + /** + * Returns fixed features */ QgsFeatureList fixedFeatures() { return mFixedFeatures; } - /* - * returns unfixed features + /** + * Returns unfixed features (canceled or not handeled) */ QgsFeatureList unfixedFeatures() { return mUnfixedFeatures; } @@ -50,11 +73,9 @@ class GUI_EXPORT QgsFixAttributeDialog : public QDialog QString descriptionText(); QgsFeatureList mFeatures; - QgsFeatureList::iterator currentFeature; + QgsFeatureList::iterator mCurrentFeature; - //the fixed features QgsFeatureList mFixedFeatures; - //the not yet fixed and the canceled features QgsFeatureList mUnfixedFeatures; QgsAttributeForm *mAttributeForm = nullptr; From 5c5ba4ef4a09baadd999d3d8521c6accdf78778c Mon Sep 17 00:00:00 2001 From: David Signer Date: Thu, 9 Jan 2020 17:05:29 +0100 Subject: [PATCH 06/10] behavior for one single feature --- src/app/qgisapp.cpp | 2 +- src/gui/qgsfixattributedialog.cpp | 32 ++++++++++++++++++++----------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 10e183a63ea2..14d3aa55bfd0 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -9643,7 +9643,7 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer ) pasteVectorLayer->updateExtents(); int nCopiedFeatures = newFeatures.count(); - Qgis::MessageLevel level = ( nCopiedFeatures == 0 || nCopiedFeatures < nTotalFeatures || invalidGeometriesCount > 0 ) ? Qgis::Warning : Qgis::Info; + Qgis::MessageLevel level = ( nCopiedFeatures == 0 || invalidGeometriesCount > 0 ) ? Qgis::Warning : Qgis::Info; QString message; if ( nCopiedFeatures == 0 ) { diff --git a/src/gui/qgsfixattributedialog.cpp b/src/gui/qgsfixattributedialog.cpp index 92d18192bc98..c790f941a261 100644 --- a/src/gui/qgsfixattributedialog.cpp +++ b/src/gui/qgsfixattributedialog.cpp @@ -29,7 +29,7 @@ QgsFixAttributeDialog::QgsFixAttributeDialog( QgsVectorLayer *vl, QgsFeatureList void QgsFixAttributeDialog::init( QgsVectorLayer *layer ) { QgsAttributeEditorContext context; - setWindowTitle( tr( "%1 - Fix Feature Attributes" ).arg( layer->name() ) ); + setWindowTitle( tr( "%1 - Fix Pastet Features" ).arg( layer->name() ) ); setLayout( new QGridLayout() ); layout()->setMargin( 0 ); context.setFormMode( QgsAttributeEditorContext::StandaloneDialog ); @@ -43,10 +43,12 @@ void QgsFixAttributeDialog::init( QgsVectorLayer *layer ) layout()->addWidget( infoBox ); mDescription = new QLabel( descriptionText() ); + mDescription->setVisible( mFeatures.count() > 1 ); infoLayout->addWidget( mDescription ); mProgressBar = new QProgressBar(); mProgressBar->setOrientation( Qt::Horizontal ); mProgressBar->setRange( 0, mFeatures.count() ); + mProgressBar->setVisible( mFeatures.count() > 1 ); infoLayout->addWidget( mProgressBar ); QgsFeature feature; mAttributeForm = new QgsAttributeForm( layer, *mCurrentFeature, context, this ); @@ -58,17 +60,25 @@ void QgsFixAttributeDialog::init( QgsVectorLayer *layer ) QPushButton *cancelAllBtn = new QPushButton( tr( "Cancel all" ) ); QPushButton *cancelAllInvalidBtn = new QPushButton( tr( "Cancel all invalid" ) ); QPushButton *storeAllInvalidBtn = new QPushButton( tr( "Store all (even invalid)" ) ); - buttonBox->addButton( cancelAllBtn, QDialogButtonBox::ActionRole ); - buttonBox->addButton( cancelAllInvalidBtn, QDialogButtonBox::ActionRole ); - buttonBox->addButton( storeAllInvalidBtn, QDialogButtonBox::ActionRole ); - connect( cancelAllBtn, &QAbstractButton::clicked, this, [ = ]() + if ( mFeatures.count() > 1 ) { - done( VanishAll ); - } ); - connect( cancelAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() + buttonBox->addButton( cancelAllBtn, QDialogButtonBox::ActionRole ); + buttonBox->addButton( cancelAllInvalidBtn, QDialogButtonBox::ActionRole ); + connect( cancelAllBtn, &QAbstractButton::clicked, this, [ = ]() + { + done( VanishAll ); + } ); + connect( cancelAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() + { + done( CopyValid ); + } ); + buttonBox->button( QDialogButtonBox::Cancel )->setText( tr( "Skip" ) ); + } + else { - done( CopyValid ); - } ); + storeAllInvalidBtn->setText( tr( "Store anyway" ) ); + } + buttonBox->addButton( storeAllInvalidBtn, QDialogButtonBox::ActionRole ); connect( storeAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() { done( CopyAll ); @@ -82,7 +92,7 @@ void QgsFixAttributeDialog::init( QgsVectorLayer *layer ) QString QgsFixAttributeDialog::descriptionText() { - return tr( "%1 of %2 features fixed\n%3 of %4 features canceled" ).arg( fixedFeatures().count() ).arg( mFeatures.count() ).arg( mCurrentFeature - mFeatures.begin() - fixedFeatures().count() ).arg( mFeatures.count() ); + return tr( "%1 of %2 features processed (%3 fixed, %4 skipped)" ).arg( mCurrentFeature - mFeatures.begin() ).arg( mFeatures.count() ).arg( mFixedFeatures.count() ).arg( mCurrentFeature - mFeatures.begin() - mFixedFeatures.count() ); } void QgsFixAttributeDialog::accept() From e906778687c9ecdd22ba8da6d3894573b59bbb15 Mon Sep 17 00:00:00 2001 From: David Signer Date: Thu, 9 Jan 2020 21:20:36 +0100 Subject: [PATCH 07/10] fix docs --- src/gui/qgsfixattributedialog.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/qgsfixattributedialog.h b/src/gui/qgsfixattributedialog.h index 334af29eb2d0..7b05e3015b7b 100644 --- a/src/gui/qgsfixattributedialog.h +++ b/src/gui/qgsfixattributedialog.h @@ -30,7 +30,7 @@ * \ingroup gui * \class QgsFixAttributeDialog * \brief Dialog to fix a list of invalid feature regarding constraints - * \since 3.12 + * \since QGIS 3.12 */ class GUI_EXPORT QgsFixAttributeDialog : public QDialog From 4574268b7b2d728d123e6e54e856537e189fd113 Mon Sep 17 00:00:00 2001 From: David Signer Date: Mon, 13 Jan 2020 08:44:04 +0100 Subject: [PATCH 08/10] use iterator for looping validFeatures better naming of the buttons and the enums --- src/app/qgisapp.cpp | 70 ++++++++++++++++++------------- src/gui/qgsfixattributedialog.cpp | 20 ++++----- src/gui/qgsfixattributedialog.h | 6 +-- 3 files changed, 54 insertions(+), 42 deletions(-) diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 14d3aa55bfd0..cb15f8245b0d 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -9591,45 +9591,57 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer ) QgsFeatureList newFeatures {QgsVectorLayerUtils::createFeatures( pasteVectorLayer, newFeaturesDataList, &context )}; // check constraints - QgsFeatureList validFeatures = newFeatures; - QgsFeatureList invalidFeatures; - for ( const QgsFeature &f : qgis::as_const( validFeatures ) ) + bool hasStrongConstraints = false; + + for ( const QgsField &field : pasteVectorLayer->fields() ) + { + if ( ( field.constraints().constraints() & QgsFieldConstraints::ConstraintUnique && !field.constraints().constraintExpression().isEmpty() && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintUnique ) & QgsFieldConstraints::ConstraintStrengthHard ) + || ( field.constraints().constraints() & QgsFieldConstraints::ConstraintNotNull && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintNotNull ) & QgsFieldConstraints::ConstraintStrengthHard ) + || ( field.constraints().constraints() & QgsFieldConstraints::ConstraintExpression && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintExpression ) & QgsFieldConstraints::ConstraintStrengthHard ) + ) + hasStrongConstraints = true; + } + + if ( hasStrongConstraints ) { - for ( int idx = 0; idx < pasteVectorLayer->fields().count(); ++idx ) + QgsFeatureList validFeatures = newFeatures; + QgsFeatureList invalidFeatures; + QMutableListIterator it( validFeatures ); + while ( it.hasNext() ) { - QStringList errors; - if ( !QgsVectorLayerUtils::validateAttribute( pasteVectorLayer, f, idx, errors, QgsFieldConstraints::ConstraintStrengthHard, QgsFieldConstraints::ConstraintOriginNotSet ) ) + QgsFeature &f = it.next(); + for ( int idx = 0; idx < pasteVectorLayer->fields().count(); ++idx ) { - invalidFeatures << f; - validFeatures.removeOne( f ); - break; + QStringList errors; + if ( !QgsVectorLayerUtils::validateAttribute( pasteVectorLayer, f, idx, errors, QgsFieldConstraints::ConstraintStrengthHard, QgsFieldConstraints::ConstraintOriginNotSet ) ) + { + invalidFeatures << f; + it.remove(); + break; + } } } - } - if ( !invalidFeatures.isEmpty() ) - { - newFeatures.clear(); + if ( !invalidFeatures.isEmpty() ) + { + newFeatures.clear(); - QgsFixAttributeDialog *dialog = new QgsFixAttributeDialog( pasteVectorLayer, invalidFeatures, this ); - int feedback = dialog->exec(); + QgsFixAttributeDialog *dialog = new QgsFixAttributeDialog( pasteVectorLayer, invalidFeatures, this ); + int feedback = dialog->exec(); - if ( feedback == QgsFixAttributeDialog::VanishAll ) - { - //vanish all - } - else if ( feedback == QgsFixAttributeDialog::CopyValid ) - { - //copy valid and fixed, vanish unfixed - newFeatures << validFeatures << dialog->fixedFeatures(); - } - else if ( feedback == QgsFixAttributeDialog::CopyAll ) - { - //copy all, even unfixed - newFeatures << validFeatures << dialog->fixedFeatures() << dialog->unfixedFeatures(); + switch ( feedback ) + { + case QgsFixAttributeDialog::PasteValid: + //paste valid and fixed, vanish unfixed + newFeatures << validFeatures << dialog->fixedFeatures(); + break; + case QgsFixAttributeDialog::PasteAll: + //paste all, even unfixed + newFeatures << validFeatures << dialog->fixedFeatures() << dialog->unfixedFeatures(); + break; + } } } - pasteVectorLayer->addFeatures( newFeatures ); QgsFeatureIds newIds; newIds.reserve( newFeatures.size() ); diff --git a/src/gui/qgsfixattributedialog.cpp b/src/gui/qgsfixattributedialog.cpp index c790f941a261..57a83bc8ff5e 100644 --- a/src/gui/qgsfixattributedialog.cpp +++ b/src/gui/qgsfixattributedialog.cpp @@ -29,7 +29,7 @@ QgsFixAttributeDialog::QgsFixAttributeDialog( QgsVectorLayer *vl, QgsFeatureList void QgsFixAttributeDialog::init( QgsVectorLayer *layer ) { QgsAttributeEditorContext context; - setWindowTitle( tr( "%1 - Fix Pastet Features" ).arg( layer->name() ) ); + setWindowTitle( tr( "%1 - Fix Pasted Features" ).arg( layer->name() ) ); setLayout( new QGridLayout() ); layout()->setMargin( 0 ); context.setFormMode( QgsAttributeEditorContext::StandaloneDialog ); @@ -57,31 +57,31 @@ void QgsFixAttributeDialog::init( QgsVectorLayer *layer ) layout()->addWidget( mAttributeForm ); QDialogButtonBox *buttonBox = mAttributeForm->findChild(); - QPushButton *cancelAllBtn = new QPushButton( tr( "Cancel all" ) ); - QPushButton *cancelAllInvalidBtn = new QPushButton( tr( "Cancel all invalid" ) ); - QPushButton *storeAllInvalidBtn = new QPushButton( tr( "Store all (even invalid)" ) ); + QPushButton *cancelAllBtn = new QPushButton( tr( "Discard All" ) ); + QPushButton *cancelAllInvalidBtn = new QPushButton( tr( "Discard All Invalid" ) ); + QPushButton *storeAllInvalidBtn = new QPushButton( tr( "Paste All (Including Invalid)" ) ); if ( mFeatures.count() > 1 ) { buttonBox->addButton( cancelAllBtn, QDialogButtonBox::ActionRole ); buttonBox->addButton( cancelAllInvalidBtn, QDialogButtonBox::ActionRole ); connect( cancelAllBtn, &QAbstractButton::clicked, this, [ = ]() { - done( VanishAll ); + done( DiscardAll ); } ); connect( cancelAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() { - done( CopyValid ); + done( PasteValid ); } ); buttonBox->button( QDialogButtonBox::Cancel )->setText( tr( "Skip" ) ); } else { - storeAllInvalidBtn->setText( tr( "Store anyway" ) ); + storeAllInvalidBtn->setText( tr( "Paste Anyway" ) ); } buttonBox->addButton( storeAllInvalidBtn, QDialogButtonBox::ActionRole ); connect( storeAllInvalidBtn, &QAbstractButton::clicked, this, [ = ]() { - done( CopyAll ); + done( PasteAll ); } ); connect( buttonBox, &QDialogButtonBox::rejected, this, &QgsFixAttributeDialog::reject ); connect( buttonBox, &QDialogButtonBox::accepted, this, &QgsFixAttributeDialog::accept ); @@ -109,7 +109,7 @@ void QgsFixAttributeDialog::accept() } else { - done( CopyValid ); + done( PasteValid ); } mProgressBar->setValue( mCurrentFeature - mFeatures.begin() ); @@ -126,7 +126,7 @@ void QgsFixAttributeDialog::reject() } else { - done( CopyValid ); + done( PasteValid ); } mProgressBar->setValue( mCurrentFeature - mFeatures.begin() ); diff --git a/src/gui/qgsfixattributedialog.h b/src/gui/qgsfixattributedialog.h index 7b05e3015b7b..631b82477fe9 100644 --- a/src/gui/qgsfixattributedialog.h +++ b/src/gui/qgsfixattributedialog.h @@ -44,9 +44,9 @@ class GUI_EXPORT QgsFixAttributeDialog : public QDialog */ enum Feedback { - VanishAll, //!< Feedback to cancel copy of all features (even valid ones) - CopyValid, //!< Feedback to copy the valid features and vanishe the invalid ones - CopyAll //!< Feedback to copy all features, no matter if valid or invalid + DiscardAll, //!< Feedback to discard all features (even valid ones) + PasteValid, //!< Feedback to paste the valid features and vanishe the invalid ones + PasteAll //!< Feedback to paste all features, no matter if valid or invalid }; /** From fb491a072aa5baa73a525689b359dd80700b7c16 Mon Sep 17 00:00:00 2001 From: David Signer Date: Mon, 13 Jan 2020 09:02:06 +0100 Subject: [PATCH 09/10] moved from gui to app --- src/app/CMakeLists.txt | 1 + src/{gui => app}/qgsfixattributedialog.cpp | 0 src/{gui => app}/qgsfixattributedialog.h | 0 src/gui/CMakeLists.txt | 2 -- 4 files changed, 1 insertion(+), 2 deletions(-) rename src/{gui => app}/qgsfixattributedialog.cpp (100%) rename src/{gui => app}/qgsfixattributedialog.h (100%) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 7ab10b97d2a9..8ed61201d94b 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -46,6 +46,7 @@ SET(QGIS_APP_SRCS qgsdisplayangle.cpp qgsfieldcalculator.cpp qgsfirstrundialog.cpp + qgsfixattributedialog.cpp qgsgeometryvalidationservice.cpp qgsgeometryvalidationdock.cpp qgsgeometryvalidationmodel.cpp diff --git a/src/gui/qgsfixattributedialog.cpp b/src/app/qgsfixattributedialog.cpp similarity index 100% rename from src/gui/qgsfixattributedialog.cpp rename to src/app/qgsfixattributedialog.cpp diff --git a/src/gui/qgsfixattributedialog.h b/src/app/qgsfixattributedialog.h similarity index 100% rename from src/gui/qgsfixattributedialog.h rename to src/app/qgsfixattributedialog.h diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 87cfd3d20bb6..03cc6742d834 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -300,7 +300,6 @@ SET(QGIS_GUI_SRCS qgsfeatureselectiondlg.cpp qgsfieldcombobox.cpp qgsfieldexpressionwidget.cpp - qgsfixattributedialog.cpp qgsfeaturelistcombobox.cpp qgsfieldvalidator.cpp qgsfieldvalueslineedit.cpp @@ -512,7 +511,6 @@ SET(QGIS_GUI_HDRS qgsfilewidget.h qgsfilterlineedit.h qgsfindfilesbypatternwidget.h - qgsfixattributedialog.h qgsfloatingwidget.h qgsfocuswatcher.h qgsfontbutton.h From 2215fde27aec1a01917a669d394f2df78f60cda4 Mon Sep 17 00:00:00 2001 From: David Signer Date: Mon, 13 Jan 2020 09:29:22 +0100 Subject: [PATCH 10/10] fix constraint check if empty --- src/app/qgisapp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index cb15f8245b0d..bd2cfbf002e1 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -9595,9 +9595,9 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer ) for ( const QgsField &field : pasteVectorLayer->fields() ) { - if ( ( field.constraints().constraints() & QgsFieldConstraints::ConstraintUnique && !field.constraints().constraintExpression().isEmpty() && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintUnique ) & QgsFieldConstraints::ConstraintStrengthHard ) + if ( ( field.constraints().constraints() & QgsFieldConstraints::ConstraintUnique && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintUnique ) & QgsFieldConstraints::ConstraintStrengthHard ) || ( field.constraints().constraints() & QgsFieldConstraints::ConstraintNotNull && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintNotNull ) & QgsFieldConstraints::ConstraintStrengthHard ) - || ( field.constraints().constraints() & QgsFieldConstraints::ConstraintExpression && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintExpression ) & QgsFieldConstraints::ConstraintStrengthHard ) + || ( field.constraints().constraints() & QgsFieldConstraints::ConstraintExpression && !field.constraints().constraintExpression().isEmpty() && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintExpression ) & QgsFieldConstraints::ConstraintStrengthHard ) ) hasStrongConstraints = true; }