diff --git a/python/core/auto_generated/qgstrackedvectorlayertools.sip.in b/python/core/auto_generated/qgstrackedvectorlayertools.sip.in index 00f5a3baff7..ca103b0fe90 100644 --- a/python/core/auto_generated/qgstrackedvectorlayertools.sip.in +++ b/python/core/auto_generated/qgstrackedvectorlayertools.sip.in @@ -41,7 +41,7 @@ This method calls the addFeature method of the backend :py:class:`QgsVectorLayer virtual bool saveEdits( QgsVectorLayer *layer ) const; - virtual bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx = 0, double dy = 0, QString *errorMsg = 0, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = 0 ) const; + virtual bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx = 0, double dy = 0, QString *errorMsg = 0, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = 0, QString *childrenInfoMsg = 0 ) const; void setVectorLayerTools( const QgsVectorLayerTools *tools ); diff --git a/python/core/auto_generated/vector/qgsvectorlayertools.sip.in b/python/core/auto_generated/vector/qgsvectorlayertools.sip.in index 35ae531f3ee..85c5a1eee2e 100644 --- a/python/core/auto_generated/vector/qgsvectorlayertools.sip.in +++ b/python/core/auto_generated/vector/qgsvectorlayertools.sip.in @@ -78,7 +78,7 @@ Should be called, when the features should be committed but the editing session %End - virtual bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request /In,Out/, double dx = 0, double dy = 0, QString *errorMsg /Out/ = 0, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = 0 ) const; + virtual bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request /In,Out/, double dx = 0, double dy = 0, QString *errorMsg /Out/ = 0, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = 0, QString *childrenInfoMsg = 0 ) const; %Docstring Copy and move features with defined translation. @@ -89,6 +89,7 @@ Copy and move features with defined translation. :param topologicalEditing: If ``True``, the function will perform topological editing of the vertices of ``layer`` on ``layer`` and ``topologicalLayer`` :param topologicalLayer: The layer where vertices from the moved features of ``layer`` will be added +:param childrenInfoMsg: If given, it will contain messages related to the creation of child features :return: - ``True`` if all features could be copied. - errorMsg: If given, it will contain the error message @@ -111,6 +112,20 @@ This flag will override the layer and general settings regarding the automatic opening of the attribute form dialog when digitizing is completed. .. versionadded:: 3.14 +%End + + void setProject( QgsProject *project ); +%Docstring +Sets the project to be used by operations when needed. + +.. versionadded:: 3.34 +%End + + QgsProject *project() const; +%Docstring +Returns the project to be used by operations when needed. + +.. versionadded:: 3.34 %End }; diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 44fcafcc22e..f1023fd5bae 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -1510,6 +1510,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipBadLayers connect( QgsApplication::messageLog(), static_cast < void ( QgsMessageLog::* )( bool ) >( &QgsMessageLog::messageReceived ), this, &QgisApp::toggleLogMessageIcon ); connect( mMessageButton, &QAbstractButton::toggled, this, &QgisApp::toggleLogMessageIcon ); mVectorLayerTools = new QgsGuiVectorLayerTools(); + mVectorLayerTools->setProject( QgsProject::instance() ); // Init the editor widget types QgsGui::editorWidgetRegistry()->initEditors( mMapCanvas, mInfoBar ); @@ -10127,148 +10128,186 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer ) return; } + const bool duplicateFeature = clipboard()->layer() == pasteVectorLayer; + pasteVectorLayer->beginEditCommand( tr( "Features pasted" ) ); QgsFeatureList features = clipboard()->transformedCopyOf( pasteVectorLayer->crs(), pasteVectorLayer->fields() ); int nTotalFeatures = features.count(); - QgsExpressionContext context = pasteVectorLayer->createExpressionContext(); - - QgsFeatureList compatibleFeatures( QgsVectorLayerUtils::makeFeaturesCompatible( features, pasteVectorLayer, QgsFeatureSink::RegeneratePrimaryKey ) ); - QgsVectorLayerUtils::QgsFeaturesDataList newFeaturesDataList; - newFeaturesDataList.reserve( compatibleFeatures.size() ); - + QgsFeatureList pastedFeatures; // Count collapsed geometries int invalidGeometriesCount = 0; - for ( const auto &feature : std::as_const( compatibleFeatures ) ) + if ( duplicateFeature ) { - - QgsGeometry geom = feature.geometry(); - - if ( !( geom.isEmpty() || geom.isNull( ) ) ) - { - // avoid intersection if enabled in digitize settings - QList avoidIntersectionsLayers; - switch ( QgsProject::instance()->avoidIntersectionsMode() ) - { - case Qgis::AvoidIntersectionsMode::AvoidIntersectionsCurrentLayer: - avoidIntersectionsLayers.append( pasteVectorLayer ); - break; - case Qgis::AvoidIntersectionsMode::AvoidIntersectionsLayers: - avoidIntersectionsLayers = QgsProject::instance()->avoidIntersectionsLayers(); - break; - case Qgis::AvoidIntersectionsMode::AllowIntersections: - break; - } - if ( avoidIntersectionsLayers.size() > 0 ) - { - geom.avoidIntersections( avoidIntersectionsLayers ); - } - - // count collapsed geometries - if ( geom.isEmpty() || geom.isNull( ) ) - invalidGeometriesCount++; - } - - QgsAttributeMap attrMap; - for ( int i = 0; i < feature.attributes().count(); i++ ) - { - attrMap[i] = feature.attribute( i ); - } - newFeaturesDataList << QgsVectorLayerUtils::QgsFeatureData( geom, attrMap ); - } - - // 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 constraints - bool hasStrongConstraints = false; - - for ( const QgsField &field : pasteVectorLayer->fields() ) - { - 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().constraintExpression().isEmpty() && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintExpression ) & QgsFieldConstraints::ConstraintStrengthHard ) - ) - { - hasStrongConstraints = true; - break; - } - } - - if ( hasStrongConstraints ) - { - QgsFeatureList validFeatures = newFeatures; - QgsFeatureList invalidFeatures; - QMutableListIterator it( validFeatures ); - while ( it.hasNext() ) - { - QgsFeature &f = it.next(); - for ( int idx = 0; idx < pasteVectorLayer->fields().count(); ++idx ) - { - QStringList errors; - if ( !QgsVectorLayerUtils::validateAttribute( pasteVectorLayer, f, idx, errors, QgsFieldConstraints::ConstraintStrengthHard, QgsFieldConstraints::ConstraintOriginNotSet ) ) - { - invalidFeatures << f; - it.remove(); - break; - } - } - } - - if ( !invalidFeatures.isEmpty() ) - { - newFeatures.clear(); - - QgsAttributeEditorContext context( createAttributeEditorContext() ); - context.setAllowCustomUi( false ); - context.setFormMode( QgsAttributeEditorContext::StandaloneDialog ); - - QgsFixAttributeDialog *dialog = new QgsFixAttributeDialog( pasteVectorLayer, invalidFeatures, this, context ); - - connect( dialog, &QgsFixAttributeDialog::finished, this, [ = ]( int feedback ) - { - QgsFeatureList features = newFeatures; - switch ( feedback ) - { - case QgsFixAttributeDialog::PasteValid: - //paste valid and fixed, vanish unfixed - features << validFeatures << dialog->fixedFeatures(); - break; - case QgsFixAttributeDialog::PasteAll: - //paste all, even unfixed - features << validFeatures << dialog->fixedFeatures() << dialog->unfixedFeatures(); - break; - } - pasteFeatures( pasteVectorLayer, invalidGeometriesCount, nTotalFeatures, features ); - dialog->deleteLater(); - } ); - dialog->show(); - return; - } - } - - pasteFeatures( pasteVectorLayer, invalidGeometriesCount, nTotalFeatures, newFeatures ); -} - -void QgisApp::pasteFeatures( QgsVectorLayer *pasteVectorLayer, int invalidGeometriesCount, int nTotalFeatures, QgsFeatureList &features ) -{ - int nCopiedFeatures = features.count(); - if ( pasteVectorLayer->addFeatures( features ) ) - { - QgsFeatureIds newIds; - newIds.reserve( features.size() ); - for ( const QgsFeature &f : std::as_const( features ) ) - { - newIds << f.id(); - } - - pasteVectorLayer->selectByIds( newIds ); + pastedFeatures = features; } else { - nCopiedFeatures = 0; + QgsExpressionContext context = pasteVectorLayer->createExpressionContext(); + QgsFeatureList compatibleFeatures( QgsVectorLayerUtils::makeFeaturesCompatible( features, pasteVectorLayer, QgsFeatureSink::RegeneratePrimaryKey ) ); + QgsVectorLayerUtils::QgsFeaturesDataList newFeaturesDataList; + newFeaturesDataList.reserve( compatibleFeatures.size() ); + + for ( const auto &feature : std::as_const( compatibleFeatures ) ) + { + QgsGeometry geom = feature.geometry(); + + if ( !( geom.isEmpty() || geom.isNull( ) ) ) + { + // avoid intersection if enabled in digitize settings + QList avoidIntersectionsLayers; + switch ( QgsProject::instance()->avoidIntersectionsMode() ) + { + case Qgis::AvoidIntersectionsMode::AvoidIntersectionsCurrentLayer: + avoidIntersectionsLayers.append( pasteVectorLayer ); + break; + case Qgis::AvoidIntersectionsMode::AvoidIntersectionsLayers: + avoidIntersectionsLayers = QgsProject::instance()->avoidIntersectionsLayers(); + break; + case Qgis::AvoidIntersectionsMode::AllowIntersections: + break; + } + if ( avoidIntersectionsLayers.size() > 0 ) + { + geom.avoidIntersections( avoidIntersectionsLayers ); + } + + // count collapsed geometries + if ( geom.isEmpty() || geom.isNull( ) ) + invalidGeometriesCount++; + } + + QgsAttributeMap attrMap; + for ( int i = 0; i < feature.attributes().count(); i++ ) + { + attrMap[i] = feature.attribute( i ); + } + newFeaturesDataList << QgsVectorLayerUtils::QgsFeatureData( geom, attrMap ); + } + + // now create new feature using pasted feature as a template. This automatically handles default + // values and field constraints + pastedFeatures = QgsVectorLayerUtils::createFeatures( pasteVectorLayer, newFeaturesDataList, &context ); + + // check constraints + bool hasStrongConstraints = false; + for ( const QgsField &field : pasteVectorLayer->fields() ) + { + 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().constraintExpression().isEmpty() && field.constraints().constraintStrength( QgsFieldConstraints::ConstraintExpression ) & QgsFieldConstraints::ConstraintStrengthHard ) + ) + { + hasStrongConstraints = true; + break; + } + } + + if ( hasStrongConstraints ) + { + QgsFeatureList validFeatures = pastedFeatures; + QgsFeatureList invalidFeatures; + QMutableListIterator it( validFeatures ); + while ( it.hasNext() ) + { + QgsFeature &f = it.next(); + for ( int idx = 0; idx < pasteVectorLayer->fields().count(); ++idx ) + { + QStringList errors; + if ( !QgsVectorLayerUtils::validateAttribute( pasteVectorLayer, f, idx, errors, QgsFieldConstraints::ConstraintStrengthHard, QgsFieldConstraints::ConstraintOriginNotSet ) ) + { + invalidFeatures << f; + it.remove(); + break; + } + } + } + + if ( !invalidFeatures.isEmpty() ) + { + pastedFeatures.clear(); + + QgsAttributeEditorContext context( createAttributeEditorContext() ); + context.setAllowCustomUi( false ); + context.setFormMode( QgsAttributeEditorContext::StandaloneDialog ); + + QgsFixAttributeDialog *dialog = new QgsFixAttributeDialog( pasteVectorLayer, invalidFeatures, this, context ); + + connect( dialog, &QgsFixAttributeDialog::finished, this, [ = ]( int feedback ) + { + QgsFeatureList features = pastedFeatures; + switch ( feedback ) + { + case QgsFixAttributeDialog::PasteValid: + //paste valid and fixed, vanish unfixed + features << validFeatures << dialog->fixedFeatures(); + break; + case QgsFixAttributeDialog::PasteAll: + //paste all, even unfixed + features << validFeatures << dialog->fixedFeatures() << dialog->unfixedFeatures(); + break; + } + pasteFeatures( pasteVectorLayer, invalidGeometriesCount, nTotalFeatures, features ); + dialog->deleteLater(); + } ); + dialog->show(); + return; + } + } } + + pasteFeatures( pasteVectorLayer, invalidGeometriesCount, nTotalFeatures, pastedFeatures, duplicateFeature ); +} + +void QgisApp::pasteFeatures( QgsVectorLayer *pasteVectorLayer, int invalidGeometriesCount, int nTotalFeatures, QgsFeatureList &features, bool duplicateFeature ) +{ + int nCopiedFeatures = features.count(); + QgsFeatureIds newIds; + newIds.reserve( features.size() ); + QString childrenInfo; + if ( duplicateFeature ) + { + QgsVectorLayerUtils::QgsDuplicateFeatureContext duplicateFeatureContext; + QMap duplicateFeatureCount; + for ( const QgsFeature &f : std::as_const( features ) ) + { + QgsFeature duplicatedFeature = QgsVectorLayerUtils::duplicateFeature( pasteVectorLayer, f, QgsProject::instance(), duplicateFeatureContext ); + newIds << duplicatedFeature.id(); + + const auto duplicateFeatureContextLayers = duplicateFeatureContext.layers(); + for ( QgsVectorLayer *chl : duplicateFeatureContextLayers ) + { + if ( duplicateFeatureCount.contains( chl->name() ) ) + { + duplicateFeatureCount[chl->name()] += duplicateFeatureContext.duplicatedFeatures( chl ).size(); + } + else + { + duplicateFeatureCount[chl->name()] = duplicateFeatureContext.duplicatedFeatures( chl ).size(); + } + } + } + + for ( auto it = duplicateFeatureCount.constBegin(); it != duplicateFeatureCount.constEnd(); ++it ) + { + childrenInfo += ( tr( "\n%n children on layer %1 duplicated", nullptr, it.value() ).arg( it.key() ) ); + } + } + else + { + if ( pasteVectorLayer->addFeatures( features ) ) + { + for ( const QgsFeature &f : std::as_const( features ) ) + { + newIds << f.id(); + } + } + else + { + nCopiedFeatures = 0; + } + } + pasteVectorLayer->selectByIds( newIds ); pasteVectorLayer->endEditCommand(); pasteVectorLayer->updateExtents(); @@ -10280,7 +10319,7 @@ void QgisApp::pasteFeatures( QgsVectorLayer *pasteVectorLayer, int invalidGeomet } else if ( nCopiedFeatures == nTotalFeatures ) { - message = tr( "%n feature(s) were pasted.", nullptr, nCopiedFeatures ); + message = tr( "%n feature(s) were pasted.%1", nullptr, nCopiedFeatures ).arg( childrenInfo ); } else { diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 6f8e538d8ef..559997172a3 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -2351,8 +2351,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow /** * Pastes the \a features to the \a pasteVectorLayer and gives feedback to the user * according to \a invalidGeometryCount and \a nTotalFeatures + * \note Setting the \a duplicateFeature to TRUE will handle the pasting of features as duplicates of pre-existing features */ - void pasteFeatures( QgsVectorLayer *pasteVectorLayer, int invalidGeometriesCount, int nTotalFeatures, QgsFeatureList &features ); + void pasteFeatures( QgsVectorLayer *pasteVectorLayer, int invalidGeometriesCount, int nTotalFeatures, QgsFeatureList &features, bool duplicateFeature = false ); /** * starts/stops for a vector layer \a vlayer diff --git a/src/app/qgsattributetabledialog.cpp b/src/app/qgsattributetabledialog.cpp index d1c5dd9ff85..14370cfb5f4 100644 --- a/src/app/qgsattributetabledialog.cpp +++ b/src/app/qgsattributetabledialog.cpp @@ -770,7 +770,6 @@ void QgsAttributeTableDialog::mActionCopySelectedRows_triggered() if ( mMainView->view() == QgsDualView::AttributeTable ) { const QList featureIds = mMainView->tableView()->selectedFeaturesIds(); - QgsFeatureStore featureStore; QgsFields fields = QgsFields( mLayer->fields() ); QStringList fieldNames; @@ -785,8 +784,9 @@ void QgsAttributeTableDialog::mActionCopySelectedRows_triggered() } fieldNames << columnConfig.name; } - featureStore.setFields( fields ); + QgsFeatureStore featureStore; + featureStore.setFields( fields ); QgsFeatureIterator it = mLayer->getFeatures( QgsFeatureRequest( qgis::listToSet( featureIds ) ) .setSubsetOfAttributes( fieldNames, mLayer->fields() ) ); QgsFeatureMap featureMap; @@ -803,7 +803,7 @@ void QgsAttributeTableDialog::mActionCopySelectedRows_triggered() featureStore.setCrs( mLayer->crs() ); - QgisApp::instance()->clipboard()->replaceWithCopyOf( featureStore ); + QgisApp::instance()->clipboard()->replaceWithCopyOf( featureStore, fields == mLayer->fields() ? mLayer : nullptr ); } else { diff --git a/src/app/qgsclipboard.cpp b/src/app/qgsclipboard.cpp index 134975e1be9..e656d89810c 100644 --- a/src/app/qgsclipboard.cpp +++ b/src/app/qgsclipboard.cpp @@ -59,6 +59,7 @@ void QgsClipboard::replaceWithCopyOf( QgsVectorLayer *src ) mFeatureFields = src->fields(); mFeatureClipboard = src->selectedFeatures(); mCRS = src->crs(); + mFeatureLayer = src; QgsDebugMsgLevel( QStringLiteral( "replaced QGIS clipboard." ), 2 ); setSystemClipboard(); @@ -99,6 +100,7 @@ void QgsClipboard::replaceWithCopyOf( QgsVectorTileLayer *src ) } mCRS = src->crs(); + mFeatureLayer = src; QgsDebugMsgLevel( QStringLiteral( "replaced QGIS clipboard." ), 2 ); setSystemClipboard(); @@ -106,12 +108,13 @@ void QgsClipboard::replaceWithCopyOf( QgsVectorTileLayer *src ) emit changed(); } -void QgsClipboard::replaceWithCopyOf( QgsFeatureStore &featureStore ) +void QgsClipboard::replaceWithCopyOf( QgsFeatureStore &featureStore, QgsVectorLayer *src ) { QgsDebugMsgLevel( QStringLiteral( "features count = %1" ).arg( featureStore.features().size() ), 2 ); mFeatureFields = featureStore.fields(); mFeatureClipboard = featureStore.features(); mCRS = featureStore.crs(); + mFeatureLayer = src; setSystemClipboard(); mUseSystemClipboard = false; emit changed(); @@ -528,6 +531,14 @@ QgsFields QgsClipboard::fields() const return retrieveFields(); } +QgsMapLayer *QgsClipboard::layer() const +{ + if ( !mUseSystemClipboard ) + return mFeatureLayer.data(); + else + return nullptr; +} + void QgsClipboard::systemClipboardChanged() { if ( mIgnoreNextSystemClipboardChange ) diff --git a/src/app/qgsclipboard.h b/src/app/qgsclipboard.h index 56e04238974..12653545eed 100644 --- a/src/app/qgsclipboard.h +++ b/src/app/qgsclipboard.h @@ -26,6 +26,7 @@ #include "qgsfields.h" #include "qgsfeature.h" #include "qgscoordinatereferencesystem.h" +#include "qgsmaplayer.h" #include "qgis_app.h" class QgsVectorLayer; @@ -80,7 +81,7 @@ class APP_EXPORT QgsClipboard : public QObject * Place a copy of features on the internal clipboard, * destroying the previous contents. */ - void replaceWithCopyOf( QgsFeatureStore &featureStore ); + void replaceWithCopyOf( QgsFeatureStore &featureStore, QgsVectorLayer *src = nullptr ); /** * Returns a copy of features on the internal clipboard. @@ -140,6 +141,8 @@ class APP_EXPORT QgsClipboard : public QObject */ QgsFields fields() const; + QgsMapLayer *layer() const; + private slots: void systemClipboardChanged(); @@ -184,6 +187,7 @@ class APP_EXPORT QgsClipboard : public QObject QgsFeatureList mFeatureClipboard; QgsFields mFeatureFields; QgsCoordinateReferenceSystem mCRS; + QPointer mFeatureLayer; //! True if next system clipboard change should be ignored bool mIgnoreNextSystemClipboardChange = false; diff --git a/src/app/qgsmaptoolmovefeature.cpp b/src/app/qgsmaptoolmovefeature.cpp index 9b388487818..f19d53313e0 100644 --- a/src/app/qgsmaptoolmovefeature.cpp +++ b/src/app/qgsmaptoolmovefeature.cpp @@ -245,13 +245,18 @@ void QgsMapToolMoveFeature::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) case CopyMove: QgsFeatureRequest request; request.setFilterFids( mMovedFeatures ); - QString *errorMsg = new QString(); - if ( !QgisApp::instance()->vectorLayerTools()->copyMoveFeatures( vlayer, request, dx, dy, errorMsg, QgsProject::instance()->topologicalEditing(), mSnapIndicator->match().layer() ) ) + QString errorMsg; + QString childrenInfoMsg; + if ( !QgisApp::instance()->vectorLayerTools()->copyMoveFeatures( vlayer, request, dx, dy, &errorMsg, QgsProject::instance()->topologicalEditing(), mSnapIndicator->match().layer(), &childrenInfoMsg ) ) { - emit messageEmitted( *errorMsg, Qgis::MessageLevel::Critical ); + emit messageEmitted( errorMsg, Qgis::MessageLevel::Critical ); deleteRubberband(); mSnapIndicator->setMatch( QgsPointLocator::Match() ); } + if ( !childrenInfoMsg.isEmpty() ) + { + emit messageEmitted( childrenInfoMsg, Qgis::MessageLevel::Info ); + } break; } diff --git a/src/core/qgstrackedvectorlayertools.cpp b/src/core/qgstrackedvectorlayertools.cpp index 10a94426d3b..3256f0fd89a 100644 --- a/src/core/qgstrackedvectorlayertools.cpp +++ b/src/core/qgstrackedvectorlayertools.cpp @@ -55,9 +55,9 @@ bool QgsTrackedVectorLayerTools::saveEdits( QgsVectorLayer *layer ) const return mBackend->saveEdits( layer ); } -bool QgsTrackedVectorLayerTools::copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx, double dy, QString *errorMsg, const bool topologicalEditing, QgsVectorLayer *topologicalLayer ) const +bool QgsTrackedVectorLayerTools::copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx, double dy, QString *errorMsg, const bool topologicalEditing, QgsVectorLayer *topologicalLayer, QString *childrenInfoMsg ) const { - return mBackend->copyMoveFeatures( layer, request, dx, dy, errorMsg, topologicalEditing, topologicalLayer ); + return mBackend->copyMoveFeatures( layer, request, dx, dy, errorMsg, topologicalEditing, topologicalLayer, childrenInfoMsg ); } void QgsTrackedVectorLayerTools::setVectorLayerTools( const QgsVectorLayerTools *tools ) diff --git a/src/core/qgstrackedvectorlayertools.h b/src/core/qgstrackedvectorlayertools.h index 23e320991f4..f12b127d9a7 100644 --- a/src/core/qgstrackedvectorlayertools.h +++ b/src/core/qgstrackedvectorlayertools.h @@ -50,7 +50,7 @@ class CORE_EXPORT QgsTrackedVectorLayerTools : public QgsVectorLayerTools bool startEditing( QgsVectorLayer *layer ) const override; bool stopEditing( QgsVectorLayer *layer, bool allowCancel ) const override; bool saveEdits( QgsVectorLayer *layer ) const override; - bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx = 0, double dy = 0, QString *errorMsg = nullptr, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = nullptr ) const override; + bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx = 0, double dy = 0, QString *errorMsg = nullptr, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = nullptr, QString *childrenInfoMsg = nullptr ) const override; /** * Set the vector layer tools that will be used to interact with the data diff --git a/src/core/vector/qgsvectorlayertools.cpp b/src/core/vector/qgsvectorlayertools.cpp index d6a22d30680..99aba7f309b 100644 --- a/src/core/vector/qgsvectorlayertools.cpp +++ b/src/core/vector/qgsvectorlayertools.cpp @@ -19,13 +19,14 @@ #include "qgsfeaturerequest.h" #include "qgslogger.h" #include "qgsvectorlayerutils.h" +#include "qgsproject.h" QgsVectorLayerTools::QgsVectorLayerTools() : QObject( nullptr ) {} -bool QgsVectorLayerTools::copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx, double dy, QString *errorMsg, const bool topologicalEditing, QgsVectorLayer *topologicalLayer ) const +bool QgsVectorLayerTools::copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request, double dx, double dy, QString *errorMsg, const bool topologicalEditing, QgsVectorLayer *topologicalLayer, QString *childrenInfoMsg ) const { bool res = false; if ( !layer || !layer->isEditable() ) @@ -41,12 +42,34 @@ bool QgsVectorLayerTools::copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureReq int noGeometryCount = 0; QgsFeatureIds fidList; - + QgsVectorLayerUtils::QgsDuplicateFeatureContext duplicateFeatureContext; + QMap duplicateFeatureCount; while ( fi.nextFeature( f ) ) { browsedFeatureCount++; - QgsFeature newFeature = QgsVectorLayerUtils::createFeature( layer, f.geometry(), f.attributes().toMap() ); + QgsFeature newFeature; + if ( mProject ) + { + newFeature = QgsVectorLayerUtils::duplicateFeature( layer, f, mProject, duplicateFeatureContext ); + + const auto duplicateFeatureContextLayers = duplicateFeatureContext.layers(); + for ( QgsVectorLayer *chl : duplicateFeatureContextLayers ) + { + if ( duplicateFeatureCount.contains( chl->name() ) ) + { + duplicateFeatureCount[chl->name()] += duplicateFeatureContext.duplicatedFeatures( chl ).size(); + } + else + { + duplicateFeatureCount[chl->name()] = duplicateFeatureContext.duplicatedFeatures( chl ).size(); + } + } + } + else + { + newFeature = QgsVectorLayerUtils::createFeature( layer, f.geometry(), f.attributes().toMap() ); + } // translate if ( newFeature.hasGeometry() ) @@ -82,9 +105,20 @@ bool QgsVectorLayerTools::copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureReq } } + QString childrenInfo; + for ( auto it = duplicateFeatureCount.constBegin(); it != duplicateFeatureCount.constEnd(); ++it ) + { + childrenInfo += ( tr( "\n%n children on layer %1 duplicated", nullptr, it.value() ).arg( it.key() ) ); + } + request = QgsFeatureRequest(); request.setFilterFids( fidList ); + if ( childrenInfoMsg && !childrenInfo.isEmpty() ) + { + childrenInfoMsg->append( childrenInfo ); + } + if ( !couldNotWriteCount && !noGeometryCount ) { res = true; diff --git a/src/core/vector/qgsvectorlayertools.h b/src/core/vector/qgsvectorlayertools.h index 600f1f34634..ac9dac7dfd7 100644 --- a/src/core/vector/qgsvectorlayertools.h +++ b/src/core/vector/qgsvectorlayertools.h @@ -25,6 +25,7 @@ class QgsFeatureRequest; class QgsVectorLayer; +class QgsProject; /** * \ingroup core @@ -111,10 +112,11 @@ class CORE_EXPORT QgsVectorLayerTools : public QObject * \param topologicalEditing If TRUE, the function will perform topological * editing of the vertices of \a layer on \a layer and \a topologicalLayer * \param topologicalLayer The layer where vertices from the moved features of \a layer will be added + * \param childrenInfoMsg If given, it will contain messages related to the creation of child features * \returns TRUE if all features could be copied. * */ - virtual bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request SIP_INOUT, double dx = 0, double dy = 0, QString *errorMsg SIP_OUT = nullptr, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = nullptr ) const; + virtual bool copyMoveFeatures( QgsVectorLayer *layer, QgsFeatureRequest &request SIP_INOUT, double dx = 0, double dy = 0, QString *errorMsg SIP_OUT = nullptr, const bool topologicalEditing = false, QgsVectorLayer *topologicalLayer = nullptr, QString *childrenInfoMsg = nullptr ) const; /** * Returns force suppress form popup status. @@ -134,8 +136,23 @@ class CORE_EXPORT QgsVectorLayerTools : public QObject */ void setForceSuppressFormPopup( bool forceSuppressFormPopup ); + /** + * Sets the project to be used by operations when needed. + * + * \since QGIS 3.34 + */ + void setProject( QgsProject *project ) { mProject = project; } + + /** + * Returns the project to be used by operations when needed. + * + * \since QGIS 3.34 + */ + QgsProject *project() const { return mProject; } + private: + QgsProject *mProject = nullptr; bool mForceSuppressFormPopup { false }; diff --git a/tests/src/python/test_qgsvectorlayertools.py b/tests/src/python/test_qgsvectorlayertools.py index c6a099b2c17..895245addb5 100644 --- a/tests/src/python/test_qgsvectorlayertools.py +++ b/tests/src/python/test_qgsvectorlayertools.py @@ -13,10 +13,15 @@ __copyright__ = 'Copyright 2015, The QGIS Project' import os from qgis.core import ( + QgsFeature, QgsFeatureRequest, + QgsPoint, QgsProject, + QgsRelation, + QgsRelationManager, QgsVectorLayer, QgsVectorLayerTools, + Qgis, ) import unittest from qgis.testing import start_app, QgisTestCase @@ -54,12 +59,38 @@ class TestQgsVectorLayerTools(QgisTestCase): cls.dbconn = 'service=\'qgis_test\'' if 'QGIS_PGTEST_DB' in os.environ: cls.dbconn = os.environ['QGIS_PGTEST_DB'] - # Create test layer + + # Create test layers cls.vl = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."someData" (geom) sql=', 'layer', 'postgres') - QgsProject.instance().addMapLayer(cls.vl) + cls.vl2 = QgsVectorLayer('Point?crs=EPSG:4326&field=id:integer(10,0)', 'points', 'memory') + f = QgsFeature(cls.vl2.fields()) + f.setGeometry(QgsPoint(1, 1)) + f.setAttributes([1]) + cls.vl2.startEditing() + cls.vl2.addFeature(f) + cls.vl2.commitChanges() + + cls.vl3 = QgsVectorLayer('NoGeometry?crs=EPSG:4326&field=point_id:integer(10,0)', 'details', 'memory') + f = QgsFeature(cls.vl3.fields()) + f.setAttributes([1]) + cls.vl3.startEditing() + cls.vl3.addFeature(f) + cls.vl3.addFeature(f) + cls.vl3.commitChanges() + + QgsProject.instance().addMapLayers([cls.vl, cls.vl2, cls.vl3]) + + relation = QgsRelation() + relation.setName('test') + relation.setReferencedLayer(cls.vl2.id()) + relation.setReferencingLayer(cls.vl3.id()) + relation.setStrength(Qgis.RelationshipStrength.Composition) + relation.addFieldPair('point_id', 'id') + QgsProject.instance().relationManager().addRelation(relation) cls.vltools = SubQgsVectorLayerTools() + cls.vltools.setProject(QgsProject.instance()) def testCopyMoveFeature(self): """ Test copy and move features""" @@ -73,6 +104,15 @@ class TestQgsVectorLayerTools(QgisTestCase): self.assertAlmostEqual(geom.asPoint().x(), -65.42) self.assertAlmostEqual(geom.asPoint().y(), 78.5) + def testCopyMoveFeatureRelationship(self): + """ Test copy and move features""" + rqst = QgsFeatureRequest() + rqst.setFilterFid(1) + self.vl2.startEditing() + (ok, rqst, msg) = self.vltools.copyMoveFeatures(self.vl2, rqst, -0.1, 0.2) + self.assertTrue(ok) + self.assertEqual(self.vl3.featureCount(), 4) + if __name__ == '__main__': unittest.main()