diff --git a/images/images.qrc b/images/images.qrc index c1f9ec46097..752e9d61041 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -583,6 +583,9 @@ themes/default/mIconSnappingSegment.svg themes/default/mIconTopologicalEditing.svg themes/default/mIconSnappingIntersection.svg + themes/default/mActionMoveFeatureCopy.svg + themes/default/mActionMoveFeatureCopyLine.svg + themes/default/mActionMoveFeatureCopyPoint.svg qgis_tips/symbol_levels.png diff --git a/images/themes/default/mActionMoveFeature.png b/images/themes/default/mActionMoveFeature.png deleted file mode 100644 index 9fdea5d2234..00000000000 Binary files a/images/themes/default/mActionMoveFeature.png and /dev/null differ diff --git a/images/themes/default/mActionMoveFeatureCopy.svg b/images/themes/default/mActionMoveFeatureCopy.svg new file mode 100644 index 00000000000..47acc20b755 --- /dev/null +++ b/images/themes/default/mActionMoveFeatureCopy.svg @@ -0,0 +1,1159 @@ + + + + + ring fill + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + ring fill + 2014-02-04 + + + Robert Szczepanek + + + + + + + + + + ring fill + + + GIS icons 0.2 + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/themes/default/mActionMoveFeatureCopyLine.svg b/images/themes/default/mActionMoveFeatureCopyLine.svg new file mode 100644 index 00000000000..478c080854b --- /dev/null +++ b/images/themes/default/mActionMoveFeatureCopyLine.svg @@ -0,0 +1,1205 @@ + + + + + ring fill + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + ring fill + 2014-02-04 + + + Robert Szczepanek + + + + + + + + + + ring fill + + + GIS icons 0.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/themes/default/mActionMoveFeatureCopyPoint.svg b/images/themes/default/mActionMoveFeatureCopyPoint.svg new file mode 100644 index 00000000000..619ad333390 --- /dev/null +++ b/images/themes/default/mActionMoveFeatureCopyPoint.svg @@ -0,0 +1,729 @@ + + + + + ring fill + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + ring fill + 2014-02-04 + + + Robert Szczepanek + + + + + + + + + + ring fill + + + GIS icons 0.2 + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/core/core.sip b/python/core/core.sip index 6adeeb3dff1..cc14357c1b4 100644 --- a/python/core/core.sip +++ b/python/core/core.sip @@ -139,6 +139,7 @@ %Include qgstextrenderer.sip %Include qgstolerance.sip %Include qgstracer.sip +%Include qgstrackedvectorlayertools.sip %Include qgsunittypes.sip %Include qgsvectordataprovider.sip %Include qgsvectorfilewriter.sip @@ -148,6 +149,7 @@ %Include qgsvectorlayereditpassthrough.sip %Include qgsvectorlayerimport.sip %Include qgsvectorlayerjoinbuffer.sip +%Include qgsvectorlayertools.sip %Include qgsvectorlayerundocommand.sip %Include qgsvectorlayerutils.sip %Include qgsvectorsimplifymethod.sip diff --git a/python/gui/qgstrackedvectorlayertools.sip b/python/core/qgstrackedvectorlayertools.sip similarity index 92% rename from python/gui/qgstrackedvectorlayertools.sip rename to python/core/qgstrackedvectorlayertools.sip index 6efd5f519b8..7b7ba477d69 100644 --- a/python/gui/qgstrackedvectorlayertools.sip +++ b/python/core/qgstrackedvectorlayertools.sip @@ -25,6 +25,7 @@ class QgsTrackedVectorLayerTools : QgsVectorLayerTools bool startEditing( QgsVectorLayer* layer ) const ; bool stopEditing( QgsVectorLayer* layer, bool allowCancel ) const ; bool saveEdits( QgsVectorLayer* layer ) const ; + bool copyMoveFeatures( QgsVectorLayer* layer, QgsFeatureRequest &request, double dx = 0, double dy = 0, QString *errorMsg = nullptr ) const; /** * Set the vector layer tools that will be used to interact with the data diff --git a/python/gui/qgsvectorlayertools.sip b/python/core/qgsvectorlayertools.sip similarity index 71% rename from python/gui/qgsvectorlayertools.sip rename to python/core/qgsvectorlayertools.sip index bd65f42011a..fcc2c8b90a1 100644 --- a/python/gui/qgsvectorlayertools.sip +++ b/python/core/qgsvectorlayertools.sip @@ -1,9 +1,12 @@ -class QgsVectorLayerTools +class QgsVectorLayerTools : QObject { %TypeHeaderCode #include %End public: + QgsVectorLayerTools(); + + virtual ~QgsVectorLayerTools(); /** * This method should/will be called, whenever a new feature will be added to the layer @@ -45,4 +48,17 @@ class QgsVectorLayerTools */ virtual bool saveEdits( QgsVectorLayer* layer ) const = 0; + /** + * Copy and move features with defined translation. + * + * @param layer The layer + * @param request The request for the features to be moved. It will be assigned to a new feature request with the newly copied features. + * @param dx The translation on x + * @param dy The translation on y + * @param errorMsg If given, it will contain the error message + * @return True if all features could be copied. + * + * TODO QGIS 3: remove const qualifier + */ + virtual bool copyMoveFeatures( QgsVectorLayer* layer, QgsFeatureRequest &request /In,Out/, double dx = 0, double dy = 0, QString* errorMsg /Out/ = nullptr ) const; }; diff --git a/python/gui/gui.sip b/python/gui/gui.sip index 88c27f32972..3587c165310 100644 --- a/python/gui/gui.sip +++ b/python/gui/gui.sip @@ -165,12 +165,10 @@ %Include qgstextannotationitem.sip %Include qgstextformatwidget.sip %Include qgstextpreview.sip -%Include qgstrackedvectorlayertools.sip %Include qgstreewidgetitem.sip %Include qgsunitselectionwidget.sip %Include qgsuserinputdockwidget.sip %Include qgsvariableeditorwidget.sip -%Include qgsvectorlayertools.sip %Include qgsvertexmarker.sip %Include attributetable/qgsattributetabledelegate.sip diff --git a/python/testing/mocked.py b/python/testing/mocked.py index 1445c7f9dba..312506dc13e 100644 --- a/python/testing/mocked.py +++ b/python/testing/mocked.py @@ -61,7 +61,6 @@ def get_iface(): canvas = QgsMapCanvas(my_iface.mainWindow()) canvas.resize(QSize(400, 400)) - my_iface.mapCanvas.return_value = canvas return my_iface diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 56931e374a3..6644b8e7c48 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -1248,6 +1248,7 @@ QgisApp::~QgisApp() delete mMapTools.mMeasureArea; delete mMapTools.mMeasureDist; delete mMapTools.mMoveFeature; + delete mMapTools.mMoveFeatureCopy; delete mMapTools.mMoveLabel; delete mMapTools.mNodeTool; delete mMapTools.mOffsetCurve; @@ -1584,6 +1585,7 @@ void QgisApp::createActions() connect( mActionCircularStringCurvePoint, SIGNAL( triggered() ), this, SLOT( circularStringCurvePoint() ) ); connect( mActionCircularStringRadius, SIGNAL( triggered() ), this, SLOT( circularStringRadius() ) ); connect( mActionMoveFeature, SIGNAL( triggered() ), this, SLOT( moveFeature() ) ); + connect( mActionMoveFeatureCopy, &QAction::triggered, this, &QgisApp::moveFeatureCopy ); connect( mActionRotateFeature, SIGNAL( triggered() ), this, SLOT( rotateFeature() ) ); connect( mActionReshapeFeatures, SIGNAL( triggered() ), this, SLOT( reshapeFeatures() ) ); @@ -1876,6 +1878,7 @@ void QgisApp::createActionGroups() mMapToolGroup->addAction( mActionCircularStringCurvePoint ); mMapToolGroup->addAction( mActionCircularStringRadius ); mMapToolGroup->addAction( mActionMoveFeature ); + mMapToolGroup->addAction( mActionMoveFeatureCopy ); mMapToolGroup->addAction( mActionRotateFeature ); mMapToolGroup->addAction( mActionOffsetCurve ); mMapToolGroup->addAction( mActionReshapeFeatures ); @@ -2328,6 +2331,25 @@ void QgisApp::createToolBars() layout->itemAt( i )->setAlignment( Qt::AlignLeft ); } + // move feature tool button + QToolButton* moveFeatureButton = new QToolButton( mDigitizeToolBar ); + moveFeatureButton->setPopupMode( QToolButton::MenuButtonPopup ); + moveFeatureButton->addAction( mActionMoveFeature ); + moveFeatureButton->addAction( mActionMoveFeatureCopy ); + QAction* defAction = mActionMoveFeature; + switch ( settings.value( QStringLiteral( "/UI/defaultMoveTool" ), 0 ).toInt() ) + { + case 0: + defAction = mActionMoveFeature; + break; + case 1: + defAction = mActionMoveFeatureCopy; + break; + }; + moveFeatureButton->setDefaultAction( defAction ); + connect( moveFeatureButton, SIGNAL( triggered( QAction * ) ), this, SLOT( toolButtonActionTriggered( QAction * ) ) ); + mDigitizeToolBar->insertWidget( mActionNodeTool, moveFeatureButton ); + //circular string digitize tool button QToolButton* tbAddCircularString = new QToolButton( mDigitizeToolBar ); tbAddCircularString->setPopupMode( QToolButton::MenuButtonPopup ); @@ -2617,6 +2639,7 @@ void QgisApp::setTheme( const QString& theThemeName ) mActionPasteFeatures->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionEditPaste.svg" ) ) ); mActionAddFeature->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionCapturePoint.svg" ) ) ); mActionMoveFeature->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionMoveFeaturePoint.svg" ) ) ); + mActionMoveFeatureCopy->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionMoveFeatureCopyPoint.svg" ) ) ); mActionRotateFeature->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionRotateFeature.svg" ) ) ); mActionReshapeFeatures->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionReshape.svg" ) ) ); mActionSplitFeatures->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionSplitFeatures.svg" ) ) ); @@ -2855,8 +2878,10 @@ void QgisApp::createCanvasTools() mMapTools.mCircularStringCurvePoint->setAction( mActionCircularStringCurvePoint ); mMapTools.mCircularStringRadius = new QgsMapToolCircularStringRadius( dynamic_cast( mMapTools.mAddFeature ), mMapCanvas ); mMapTools.mCircularStringRadius->setAction( mActionCircularStringRadius ); - mMapTools.mMoveFeature = new QgsMapToolMoveFeature( mMapCanvas ); + mMapTools.mMoveFeature = new QgsMapToolMoveFeature( mMapCanvas, QgsMapToolMoveFeature::Move ); mMapTools.mMoveFeature->setAction( mActionMoveFeature ); + mMapTools.mMoveFeatureCopy = new QgsMapToolMoveFeature( mMapCanvas, QgsMapToolMoveFeature::CopyMove ); + mMapTools.mMoveFeatureCopy->setAction( mActionMoveFeatureCopy ); mMapTools.mRotateFeature = new QgsMapToolRotateFeature( mMapCanvas ); mMapTools.mRotateFeature->setAction( mActionRotateFeature ); mMapTools.mOffsetCurve = new QgsMapToolOffsetCurve( mMapCanvas ); @@ -6485,6 +6510,11 @@ void QgisApp::moveFeature() mMapCanvas->setMapTool( mMapTools.mMoveFeature ); } +void QgisApp::moveFeatureCopy() +{ + mMapCanvas->setMapTool( mMapTools.mMoveFeatureCopy ); +} + void QgisApp::offsetCurve() { mMapCanvas->setMapTool( mMapTools.mOffsetCurve ); @@ -10526,6 +10556,7 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer* layer ) mActionCircularStringCurvePoint->setEnabled( false ); mActionCircularStringRadius->setEnabled( false ); mActionMoveFeature->setEnabled( false ); + mActionMoveFeatureCopy->setEnabled( false ); mActionRotateFeature->setEnabled( false ); mActionOffsetCurve->setEnabled( false ); mActionNodeTool->setEnabled( false ); @@ -10680,6 +10711,7 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer* layer ) mActionAddPart->setEnabled( isEditable && canChangeGeometry ); mActionDeletePart->setEnabled( isEditable && canChangeGeometry ); mActionMoveFeature->setEnabled( isEditable && canChangeGeometry ); + mActionMoveFeatureCopy->setEnabled( isEditable && canChangeGeometry ); mActionRotateFeature->setEnabled( isEditable && canChangeGeometry ); mActionNodeTool->setEnabled( isEditable && canChangeGeometry ); @@ -10690,6 +10722,7 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer* layer ) { mActionAddFeature->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionCapturePoint.svg" ) ) ); mActionMoveFeature->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionMoveFeaturePoint.svg" ) ) ); + mActionMoveFeatureCopy->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionMoveFeatureCopyPoint.svg" ) ) ); mActionAddRing->setEnabled( false ); mActionFillRing->setEnabled( false ); @@ -10718,6 +10751,7 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer* layer ) { mActionAddFeature->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionCaptureLine.svg" ) ) ); mActionMoveFeature->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionMoveFeatureLine.svg" ) ) ); + mActionMoveFeatureCopy->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionMoveFeatureCopyLine.svg" ) ) ); mActionReshapeFeatures->setEnabled( isEditable && canChangeGeometry ); mActionSplitFeatures->setEnabled( isEditable && canAddFeatures ); @@ -10733,6 +10767,7 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer* layer ) { mActionAddFeature->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionCapturePolygon.svg" ) ) ); mActionMoveFeature->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionMoveFeature.svg" ) ) ); + mActionMoveFeatureCopy->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionMoveFeatureCopy.svg" ) ) ); mActionAddRing->setEnabled( isEditable && canChangeGeometry ); mActionFillRing->setEnabled( isEditable && canChangeGeometry ); @@ -10819,6 +10854,7 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer* layer ) mActionAddPart->setEnabled( false ); mActionNodeTool->setEnabled( false ); mActionMoveFeature->setEnabled( false ); + mActionMoveFeatureCopy->setEnabled( false ); mActionRotateFeature->setEnabled( false ); mActionOffsetCurve->setEnabled( false ); mActionCopyFeatures->setEnabled( false ); @@ -11846,6 +11882,10 @@ void QgisApp::toolButtonActionTriggered( QAction *action ) settings.setValue( QStringLiteral( "/UI/defaultMapService" ), 0 ); else if ( action == mActionAddAmsLayer ) settings.setValue( QStringLiteral( "/UI/defaultMapService" ), 1 ); + else if ( action == mActionMoveFeature ) + settings.setValue( QStringLiteral( "/UI/defaultMoveTool" ), 0 ); + else if ( action == mActionMoveFeatureCopy ) + settings.setValue( QStringLiteral( "/UI/defaultMoveTool" ), 1 ); bt->setDefaultAction( action ); } diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index d8472274d5b..cbf2a7a393d 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -331,6 +331,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QAction *actionDeleteSelected() { return mActionDeleteSelected; } QAction *actionAddFeature() { return mActionAddFeature; } QAction *actionMoveFeature() { return mActionMoveFeature; } + QAction *actionMoveFeatureCopy() { return mActionMoveFeatureCopy; } QAction *actionRotateFeature() { return mActionRotateFeature;} QAction *actionSplitFeatures() { return mActionSplitFeatures; } QAction *actionSplitParts() { return mActionSplitParts; } @@ -1068,6 +1069,8 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow void circularStringRadius(); //! activates the move feature tool void moveFeature(); + //! activates the copy and move feature tool + void moveFeatureCopy(); //! activates the offset curve tool void offsetCurve(); //! activates the reshape features tool @@ -1634,6 +1637,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QgsMapTool *mCircularStringCurvePoint; QgsMapTool *mCircularStringRadius; QgsMapTool *mMoveFeature; + QgsMapTool *mMoveFeatureCopy; QgsMapTool *mOffsetCurve; QgsMapTool *mReshapeFeatures; QgsMapTool *mSplitFeatures; diff --git a/src/app/qgsguivectorlayertools.cpp b/src/app/qgsguivectorlayertools.cpp index bcca09eacd8..1bb4d33c5ac 100644 --- a/src/app/qgsguivectorlayertools.cpp +++ b/src/app/qgsguivectorlayertools.cpp @@ -17,19 +17,21 @@ #include #include "qgsguivectorlayertools.h" -#include "qgsvectorlayer.h" -#include "qgsvectordataprovider.h" -#include "qgsmessagebar.h" + #include "qgisapp.h" #include "qgsapplication.h" -#include "qgsmessageviewer.h" #include "qgsfeatureaction.h" +#include "qgslogger.h" #include "qgsmapcanvas.h" +#include "qgsmessagebar.h" #include "qgsmessagebaritem.h" +#include "qgsmessageviewer.h" +#include "qgsvectordataprovider.h" +#include "qgsvectorlayer.h" QgsGuiVectorLayerTools::QgsGuiVectorLayerTools() - : QObject( nullptr ) + : QgsVectorLayerTools() {} bool QgsGuiVectorLayerTools::addFeature( QgsVectorLayer* layer, const QgsAttributeMap& defaultValues, const QgsGeometry& defaultGeometry, QgsFeature* feat ) const @@ -95,7 +97,6 @@ bool QgsGuiVectorLayerTools::saveEdits( QgsVectorLayer* layer ) const return res; } - bool QgsGuiVectorLayerTools::stopEditing( QgsVectorLayer* layer, bool allowCancel ) const { bool res = true; diff --git a/src/app/qgsguivectorlayertools.h b/src/app/qgsguivectorlayertools.h index da90b64b82d..ecc57ce7664 100644 --- a/src/app/qgsguivectorlayertools.h +++ b/src/app/qgsguivectorlayertools.h @@ -23,7 +23,7 @@ * or a feature is added. */ -class QgsGuiVectorLayerTools : public QObject, public QgsVectorLayerTools +class QgsGuiVectorLayerTools : public QgsVectorLayerTools { Q_OBJECT @@ -73,6 +73,7 @@ class QgsGuiVectorLayerTools : public QObject, public QgsVectorLayerTools private: void commitError( QgsVectorLayer* vlayer ) const; + }; #endif // QGSGUIVECTORLAYERTOOLS_H diff --git a/src/app/qgsmaptoolmovefeature.cpp b/src/app/qgsmaptoolmovefeature.cpp index e4d43d0e60e..77dfa8a73fc 100644 --- a/src/app/qgsmaptoolmovefeature.cpp +++ b/src/app/qgsmaptoolmovefeature.cpp @@ -13,27 +13,39 @@ * * ***************************************************************************/ -#include "qgsmaptoolmovefeature.h" +#include "qgisapp.h" +#include "qgsadvanceddigitizingdockwidget.h" #include "qgsfeatureiterator.h" #include "qgsgeometry.h" #include "qgslogger.h" #include "qgsmapcanvas.h" +#include "qgsmaptoolmovefeature.h" #include "qgsrubberband.h" -#include "qgsvectorlayer.h" #include "qgstolerance.h" -#include "qgisapp.h" -#include "qgsadvanceddigitizingdockwidget.h" +#include "qgsvectorlayer.h" +#include "qgsvectorlayertools.h" + #include #include #include -QgsMapToolMoveFeature::QgsMapToolMoveFeature( QgsMapCanvas* canvas ) + +QgsMapToolMoveFeature::QgsMapToolMoveFeature( QgsMapCanvas* canvas , MoveMode mode ) : QgsMapToolAdvancedDigitizing( canvas, QgisApp::instance()->cadDockWidget() ) , mRubberBand( nullptr ) + , mMode( mode ) { mToolName = tr( "Move feature" ); - mCaptureMode = QgsMapToolAdvancedDigitizing::CaptureSegment; + switch ( mode ) + { + case Move: + mCaptureMode = QgsMapToolAdvancedDigitizing::CaptureSegment; + break; + case CopyMove: + mCaptureMode = QgsMapToolAdvancedDigitizing::CaptureLine; // we copy/move several times + break; + } } QgsMapToolMoveFeature::~QgsMapToolMoveFeature() @@ -138,12 +150,12 @@ void QgsMapToolMoveFeature::cadCanvasReleaseEvent( QgsMapMouseEvent* e ) } else { - delete mRubberBand; - mRubberBand = nullptr; - + // copy and move mode if ( e->button() != Qt::LeftButton ) { cadDockWidget()->clear(); + delete mRubberBand; + mRubberBand = nullptr; return; } @@ -152,13 +164,35 @@ void QgsMapToolMoveFeature::cadCanvasReleaseEvent( QgsMapMouseEvent* e ) double dx = stopPointLayerCoords.x() - startPointLayerCoords.x(); double dy = stopPointLayerCoords.y() - startPointLayerCoords.y(); - vlayer->beginEditCommand( tr( "Feature moved" ) ); - Q_FOREACH ( QgsFeatureId id, mMovedFeatures ) + + + vlayer->beginEditCommand( mMode == Move ? tr( "Feature moved" ) : tr( "Feature copied and moved" ) ); + + + switch ( mMode ) { - vlayer->translateFeature( id, dx, dy ); + case Move: + Q_FOREACH ( QgsFeatureId id, mMovedFeatures ) + { + vlayer->translateFeature( id, dx, dy ); + } + delete mRubberBand; + mRubberBand = nullptr; + break; + + case CopyMove: + QgsFeatureRequest request; + request.setFilterFids( mMovedFeatures ); + QString* errorMsg = new QString(); + if ( !QgisApp::instance()->vectorLayerTools()->copyMoveFeatures( vlayer, request, dx, dy, errorMsg ) ) + { + emit messageEmitted( *errorMsg, QgsMessageBar::CRITICAL ); + delete mRubberBand; + mRubberBand = nullptr; + } + break; } - delete mRubberBand; - mRubberBand = nullptr; + vlayer->endEditCommand(); vlayer->triggerRepaint(); } diff --git a/src/app/qgsmaptoolmovefeature.h b/src/app/qgsmaptoolmovefeature.h index fd888423155..eda9c22712e 100644 --- a/src/app/qgsmaptoolmovefeature.h +++ b/src/app/qgsmaptoolmovefeature.h @@ -23,7 +23,14 @@ class APP_EXPORT QgsMapToolMoveFeature: public QgsMapToolAdvancedDigitizing { Q_OBJECT public: - QgsMapToolMoveFeature( QgsMapCanvas* canvas ); + //! Mode for moving features + enum MoveMode + { + Move, //!< Move feature + CopyMove //!< Copy and move feature + }; + + QgsMapToolMoveFeature( QgsMapCanvas* canvas, MoveMode mode = Move ); virtual ~QgsMapToolMoveFeature(); virtual void cadCanvasMoveEvent( QgsMapMouseEvent* e ) override; @@ -45,6 +52,8 @@ class APP_EXPORT QgsMapToolMoveFeature: public QgsMapToolAdvancedDigitizing QPoint mPressPos; + MoveMode mMode; + }; #endif diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 803bbc6dd2a..ca808a861bc 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -213,6 +213,7 @@ SET(QGIS_CORE_SRCS qgstextrenderer.cpp qgstolerance.cpp qgstracer.cpp + qgstrackedvectorlayertools.cpp qgstransaction.cpp qgstransactiongroup.cpp qgsunittypes.cpp @@ -230,6 +231,7 @@ SET(QGIS_CORE_SRCS qgsvectorlayerlabeling.cpp qgsvectorlayerlabelprovider.cpp qgsvectorlayerrenderer.cpp + qgsvectorlayertools.cpp qgsvectorlayerundocommand.cpp qgsvectorlayerutils.cpp qgsvectorsimplifymethod.cpp @@ -491,6 +493,7 @@ SET(QGIS_CORE_MOC_HDRS qgsrunprocess.h qgssnappingutils.h qgstracer.h + qgstrackedvectorlayertools.h qgstransaction.h qgstransactiongroup.h qgsunittypes.h @@ -500,6 +503,7 @@ SET(QGIS_CORE_MOC_HDRS qgsvectorlayereditpassthrough.h qgsvectorlayer.h qgsvectorlayerjoinbuffer.h + qgsvectorlayertools.h qgsmapthemecollection.h qgswebpage.h qgswebview.h diff --git a/src/gui/qgstrackedvectorlayertools.cpp b/src/core/qgstrackedvectorlayertools.cpp similarity index 90% rename from src/gui/qgstrackedvectorlayertools.cpp rename to src/core/qgstrackedvectorlayertools.cpp index a09687d1e2d..b29bad54c2f 100644 --- a/src/gui/qgstrackedvectorlayertools.cpp +++ b/src/core/qgstrackedvectorlayertools.cpp @@ -17,7 +17,7 @@ #include "qgsvectorlayer.h" QgsTrackedVectorLayerTools::QgsTrackedVectorLayerTools() - : mBackend( nullptr ) + : mBackend() { } @@ -57,6 +57,11 @@ bool QgsTrackedVectorLayerTools::saveEdits( QgsVectorLayer* layer ) const return mBackend->saveEdits( layer ); } +bool QgsTrackedVectorLayerTools::copyMoveFeatures( QgsVectorLayer* layer, QgsFeatureRequest& request, double dx, double dy, QString* errorMsg ) const +{ + return mBackend->copyMoveFeatures( layer, request, dx, dy, errorMsg ); +} + void QgsTrackedVectorLayerTools::setVectorLayerTools( const QgsVectorLayerTools* tools ) { mBackend = tools; diff --git a/src/gui/qgstrackedvectorlayertools.h b/src/core/qgstrackedvectorlayertools.h similarity index 89% rename from src/gui/qgstrackedvectorlayertools.h rename to src/core/qgstrackedvectorlayertools.h index d7894e79678..32ef3a78267 100644 --- a/src/gui/qgstrackedvectorlayertools.h +++ b/src/core/qgstrackedvectorlayertools.h @@ -21,8 +21,9 @@ /** \ingroup gui * \class QgsTrackedVectorLayerTools */ -class GUI_EXPORT QgsTrackedVectorLayerTools : public QgsVectorLayerTools +class CORE_EXPORT QgsTrackedVectorLayerTools : public QgsVectorLayerTools { + Q_OBJECT public: QgsTrackedVectorLayerTools(); @@ -30,6 +31,7 @@ class GUI_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 override; /** * Set the vector layer tools that will be used to interact with the data diff --git a/src/core/qgsvectorlayertools.cpp b/src/core/qgsvectorlayertools.cpp new file mode 100644 index 00000000000..3ca7efb5e5f --- /dev/null +++ b/src/core/qgsvectorlayertools.cpp @@ -0,0 +1,107 @@ +/*************************************************************************** + qgsvectorlayertools.cpp + --------------------- + begin : 09.11.2016 + copyright : (C) 2016 by Denis Rouzaud + email : denis.rouzaud@gmail.com + *************************************************************************** + * * + * 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 "qgsvectorlayer.h" +#include "qgsvectorlayertools.h" +#include "qgsfeaturerequest.h" +#include "qgslogger.h" + + +QgsVectorLayerTools::QgsVectorLayerTools() + : QObject( nullptr ) +{} + + +QgsVectorLayerTools::~QgsVectorLayerTools() +{} + + +bool QgsVectorLayerTools::copyMoveFeatures( QgsVectorLayer* layer, QgsFeatureRequest& request, double dx, double dy, QString* errorMsg ) const +{ + bool res = false; + if ( !layer || !layer->isEditable() ) + { + return false; + } + + QgsFeatureIterator fi = layer->getFeatures( request ); + QgsFeature f; + QgsAttributeList pkAttrList = layer->pkAttributeList(); + + int browsedFeatureCount = 0; + int couldNotWriteCount = 0; + int noGeometryCount = 0; + + QgsFeatureIds fidList; + + while ( fi.nextFeature( f ) ) + { + browsedFeatureCount++; + // remove pkey values + Q_FOREACH ( auto idx, pkAttrList ) + { + f.setAttribute( idx, QVariant() ); + } + // translate + if ( f.hasGeometry() ) + { + QgsGeometry geom = f.geometry(); + geom.translate( dx, dy ); + f.setGeometry( geom ); +#ifdef QGISDEBUG + const QgsFeatureId fid = f.id(); +#endif + // paste feature + if ( !layer->addFeature( f, false ) ) + { + couldNotWriteCount++; + QgsDebugMsg( QString( "Could not add new feature. Original copied feature id: %1" ).arg( fid ) ); + } + else + { + fidList.insert( f.id() ); + } + } + else + { + noGeometryCount++; + } + } + + request = QgsFeatureRequest(); + request.setFilterFids( fidList ); + + if ( !couldNotWriteCount && !noGeometryCount ) + { + res = true; + } + else if ( errorMsg ) + { + errorMsg = new QString( QString( tr( "Only %1 out of %2 features were copied." ) ) + .arg( browsedFeatureCount - couldNotWriteCount - noGeometryCount, browsedFeatureCount ) ); + if ( noGeometryCount ) + { + errorMsg->append( " " ); + errorMsg->append( tr( "Some features have no geometry." ) ); + } + if ( couldNotWriteCount ) + { + errorMsg->append( " " ); + errorMsg->append( tr( "Some could not be created on the layer." ) ); + } + } + return res; +} diff --git a/src/gui/qgsvectorlayertools.h b/src/core/qgsvectorlayertools.h similarity index 80% rename from src/gui/qgsvectorlayertools.h rename to src/core/qgsvectorlayertools.h index 468f1d8ec42..6a9dab28eec 100644 --- a/src/gui/qgsvectorlayertools.h +++ b/src/core/qgsvectorlayertools.h @@ -16,9 +16,12 @@ #ifndef QGSVECTORLAYERTOOLS_H #define QGSVECTORLAYERTOOLS_H +#include + #include "qgsfeature.h" #include "qgsgeometry.h" +class QgsFeatureRequest; class QgsVectorLayer; /** \ingroup gui @@ -30,12 +33,14 @@ class QgsVectorLayer; * in your application. * */ -class GUI_EXPORT QgsVectorLayerTools +class CORE_EXPORT QgsVectorLayerTools : public QObject { - public: - QgsVectorLayerTools() {} + Q_OBJECT - virtual ~QgsVectorLayerTools() {} + public: + QgsVectorLayerTools(); + + virtual ~QgsVectorLayerTools(); /** * This method should/will be called, whenever a new feature will be added to the layer @@ -85,6 +90,20 @@ class GUI_EXPORT QgsVectorLayerTools */ virtual bool saveEdits( QgsVectorLayer* layer ) const = 0; + /** + * Copy and move features with defined translation. + * + * @param layer The layer + * @param request The request for the features to be moved. It will be assigned to a new feature request with the newly copied features. + * @param dx The translation on x + * @param dy The translation on y + * @param errorMsg If given, it will contain the error message + * @return True if all features could be copied. + * + * TODO QGIS 3: remove const qualifier + */ + virtual bool copyMoveFeatures( QgsVectorLayer* layer, QgsFeatureRequest &request, double dx = 0, double dy = 0, QString *errorMsg = nullptr ) const; + }; #endif // QGSVECTORLAYERTOOLS_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 3dceb74b66e..fe02b7cade1 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -311,7 +311,6 @@ SET(QGIS_GUI_SRCS qgstextannotationitem.cpp qgstextformatwidget.cpp qgstextpreview.cpp - qgstrackedvectorlayertools.cpp qgstreewidgetitem.cpp qgsunitselectionwidget.cpp qgsuserinputdockwidget.cpp @@ -662,9 +661,7 @@ SET(QGIS_GUI_HDRS qgssvgannotationitem.h qgstablewidgetitem.h qgstextannotationitem.h - qgstrackedvectorlayertools.h qgsuserinputdockwidget.h - qgsvectorlayertools.h qgsvertexmarker.h qgsfiledownloader.h diff --git a/src/ui/qgisapp.ui b/src/ui/qgisapp.ui index 86a9c1b33d2..f69671d8bf4 100644 --- a/src/ui/qgisapp.ui +++ b/src/ui/qgisapp.ui @@ -17,7 +17,7 @@ 0 0 1018 - 28 + 22 @@ -369,7 +369,6 @@ - @@ -2564,6 +2563,21 @@ Acts on currently active editable layer F3 + + + true + + + + :/images/themes/default/mActionMoveFeatureCopy.svg:/images/themes/default/mActionMoveFeatureCopy.svg + + + Copy and Move Feature(s) + + + Copy and Move Feature(s) + + diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 6f013558427..3ee40185163 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -143,6 +143,7 @@ ENDIF (WITH_DESKTOP) IF (ENABLE_PGTEST) ADD_PYTHON_TEST(PyQgsPostgresProvider test_provider_postgres.py) ADD_PYTHON_TEST(PyQgsRelationEditWidget test_qgsrelationeditwidget.py) + ADD_PYTHON_TEST(PyQgsVectorLayerTools test_qgsvectorlayertools.py) ENDIF (ENABLE_PGTEST) IF (ENABLE_MSSQLTEST) diff --git a/tests/src/python/test_qgsrelationeditwidget.py b/tests/src/python/test_qgsrelationeditwidget.py index be4877519db..d184500b4f8 100644 --- a/tests/src/python/test_qgsrelationeditwidget.py +++ b/tests/src/python/test_qgsrelationeditwidget.py @@ -23,14 +23,14 @@ from qgis.core import ( QgsRelation, QgsMapLayerRegistry, QgsTransaction, - QgsFeatureRequest + QgsFeatureRequest, + QgsVectorLayerTools ) from qgis.gui import ( QgsEditorWidgetRegistry, QgsRelationWidgetWrapper, - QgsAttributeEditorContext, - QgsVectorLayerTools + QgsAttributeEditorContext ) from qgis.PyQt.QtCore import QTimer diff --git a/tests/src/python/test_qgsvectorlayertools.py b/tests/src/python/test_qgsvectorlayertools.py new file mode 100644 index 00000000000..2fcd4612897 --- /dev/null +++ b/tests/src/python/test_qgsvectorlayertools.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit test utils for provider tests. + +.. note:: 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. +""" + +from builtins import str +from builtins import object +__author__ = 'Denis Rouzaud' +__date__ = '2016-11-07' +__copyright__ = 'Copyright 2015, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +from qgis.core import QgsFeatureRequest, QgsVectorLayer, QgsMapLayerRegistry, QgsVectorLayerTools +from qgis.testing import start_app, unittest + +import os + +start_app() + + +class SubQgsVectorLayerTools(QgsVectorLayerTools): + + def __init__(self): + super().__init__() + + def addFeature(self, layer): + pass + + def startEditing(self, layer): + pass + + def stopEditing(self, layer): + pass + + def saveEdits(self, layer): + pass + + +class TestQgsVectorLayerTools(unittest.TestCase): + + @classmethod + def setUpClass(cls): + """ + Setup the involved layers and relations for a n:m relation + :return: + """ + cls.dbconn = 'service=\'qgis_test\'' + if 'QGIS_PGTEST_DB' in os.environ: + cls.dbconn = os.environ['QGIS_PGTEST_DB'] + # Create test layer + cls.vl = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."someData" (geom) sql=', 'layer', 'postgres') + + QgsMapLayerRegistry.instance().addMapLayer(cls.vl) + + cls.vltools = SubQgsVectorLayerTools() + + def testCopyMoveFeature(self): + """ Test copy and move features""" + rqst = QgsFeatureRequest() + rqst.setFilterFid(4) + self.vl.startEditing() + (ok, rqst, msg) = self.vltools.copyMoveFeatures(self.vl, rqst, -0.1, 0.2) + self.assertTrue(ok) + for f in self.vl.getFeatures(rqst): + geom = f.geometry() + self.assertAlmostEqual(geom.asPoint().x(), -65.42) + self.assertAlmostEqual(geom.asPoint().y(), 78.5) + + +if __name__ == '__main__': + unittest.main()