From ea326827ae5dea1cab86a315e8a4293ae168db36 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 May 2020 12:57:25 +1000 Subject: [PATCH] Move 'coordinate capture' functionality to map canvas Instead of requiring users to enable the core c++ plugin 'Coordinate Capture' in order to copy coordinates for a map point, instead add this functionality to a map canvas right click context menu. Now, when compatible map tools are activated (pan, zoom, select by rect), right clicking on the canvas shows a context menu with a "Copy Coordinate" submenu. The submenu shows options for copying the coordinate in the map CRS, WGS84 or a custom preset CRS. Also adds API to allow 3rd party QgsMapTool subclasses to implement their own context menus which include this Copy Coordinate action. --- python/gui/auto_generated/qgsmapcanvas.sip.in | 1 + python/gui/auto_generated/qgsmaptool.sip.in | 19 ++++ .../gui/auto_generated/qgsmaptoolpan.sip.in | 1 + .../gui/auto_generated/qgsmaptoolzoom.sip.in | 1 + src/app/qgsmaptoolselect.cpp | 16 ++++ src/app/qgsmaptoolselect.h | 1 + src/gui/qgsmapcanvas.cpp | 95 ++++++++++++++++++- src/gui/qgsmapcanvas.h | 8 ++ src/gui/qgsmaptool.cpp | 5 + src/gui/qgsmaptool.h | 18 ++++ src/gui/qgsmaptoolpan.cpp | 6 +- src/gui/qgsmaptoolpan.h | 2 +- src/gui/qgsmaptoolzoom.cpp | 4 + src/gui/qgsmaptoolzoom.h | 2 +- 14 files changed, 172 insertions(+), 7 deletions(-) diff --git a/python/gui/auto_generated/qgsmapcanvas.sip.in b/python/gui/auto_generated/qgsmapcanvas.sip.in index 2d769842e0ff..4949e954167d 100644 --- a/python/gui/auto_generated/qgsmapcanvas.sip.in +++ b/python/gui/auto_generated/qgsmapcanvas.sip.in @@ -20,6 +20,7 @@ + class QgsMapCanvas : QGraphicsView { %Docstring diff --git a/python/gui/auto_generated/qgsmaptool.sip.in b/python/gui/auto_generated/qgsmaptool.sip.in index 2c6b12ccc77e..233be2413688 100644 --- a/python/gui/auto_generated/qgsmaptool.sip.in +++ b/python/gui/auto_generated/qgsmaptool.sip.in @@ -52,6 +52,7 @@ implemented as map tools. Transient, EditTool, AllowZoomRect, + ShowContextMenu, }; typedef QFlags Flags; @@ -191,6 +192,24 @@ Gets search radius in map units for given canvas. Used by identify, tip etc. The values is calculated from searchRadiusMM(). .. versionadded:: 2.3 +%End + + virtual void populateContextMenu( QMenu *menu ); +%Docstring +Allows the tool to populate and customize the given ``menu``, +prior to showing it in response to a right-mouse button click. + +``menu`` will be initially populated with a set of default, generic actions. +Any new actions added to the menu should be correctly parented to ``menu``. + +The default implementation does nothing. + +.. note:: + + The context menu is only shown when the ShowContextMenu flag + is present in flags(). + +.. versionadded:: 3.14 %End signals: diff --git a/python/gui/auto_generated/qgsmaptoolpan.sip.in b/python/gui/auto_generated/qgsmaptoolpan.sip.in index 7e75fd3bd103..de1ccbe74140 100644 --- a/python/gui/auto_generated/qgsmaptoolpan.sip.in +++ b/python/gui/auto_generated/qgsmaptoolpan.sip.in @@ -34,6 +34,7 @@ constructor virtual Flags flags() const; + virtual void canvasPressEvent( QgsMapMouseEvent *e ); virtual void canvasMoveEvent( QgsMapMouseEvent *e ); diff --git a/python/gui/auto_generated/qgsmaptoolzoom.sip.in b/python/gui/auto_generated/qgsmaptoolzoom.sip.in index 9e3e7639a2d4..bddce017dc70 100644 --- a/python/gui/auto_generated/qgsmaptoolzoom.sip.in +++ b/python/gui/auto_generated/qgsmaptoolzoom.sip.in @@ -28,6 +28,7 @@ constructor ~QgsMapToolZoom(); virtual Flags flags() const; + virtual void canvasMoveEvent( QgsMapMouseEvent *e ); virtual void canvasPressEvent( QgsMapMouseEvent *e ); diff --git a/src/app/qgsmaptoolselect.cpp b/src/app/qgsmaptoolselect.cpp index 0fc026c0728b..12a70389318d 100644 --- a/src/app/qgsmaptoolselect.cpp +++ b/src/app/qgsmaptoolselect.cpp @@ -122,6 +122,22 @@ void QgsMapToolSelect::deactivate() QgsMapTool::deactivate(); } +QgsMapTool::Flags QgsMapToolSelect::flags() const +{ + switch ( mSelectionHandler->selectionMode() ) + { + case QgsMapToolSelectionHandler::SelectPolygon: + break; + + case QgsMapToolSelectionHandler::SelectSimple: + case QgsMapToolSelectionHandler::SelectFreehand: + case QgsMapToolSelectionHandler::SelectRadius: + return QgsMapTool::flags() | QgsMapTool::ShowContextMenu; + } + + return QgsMapTool::flags(); +} + void QgsMapToolSelect::selectFeatures( Qt::KeyboardModifiers modifiers ) { if ( mSelectionHandler->selectionMode() == QgsMapToolSelectionHandler::SelectSimple && diff --git a/src/app/qgsmaptoolselect.h b/src/app/qgsmaptoolselect.h index 8b09c5a4272c..48207e4883af 100644 --- a/src/app/qgsmaptoolselect.h +++ b/src/app/qgsmaptoolselect.h @@ -50,6 +50,7 @@ class APP_EXPORT QgsMapToolSelect : public QgsMapTool void keyPressEvent( QKeyEvent *e ) override; void keyReleaseEvent( QKeyEvent *e ) override; void deactivate() override; + Flags flags() const override; signals: diff --git a/src/gui/qgsmapcanvas.cpp b/src/gui/qgsmapcanvas.cpp index 7c79f24ff444..57e69bdd3790 100644 --- a/src/gui/qgsmapcanvas.cpp +++ b/src/gui/qgsmapcanvas.cpp @@ -37,6 +37,8 @@ email : sherman at mrcc.com #include #include #include +#include +#include #include "qgis.h" #include "qgssettings.h" @@ -80,6 +82,7 @@ email : sherman at mrcc.com #include "qgsvectorlayertemporalproperties.h" #include "qgstemporalcontroller.h" #include "qgsruntimeprofiler.h" +#include "qgsprojectionselectiondialog.h" /** * \ingroup gui @@ -114,6 +117,7 @@ class QgsMapCanvas::CanvasProperties QgsMapCanvas::QgsMapCanvas( QWidget *parent ) : QGraphicsView( parent ) , mCanvasProperties( new CanvasProperties ) + , mMenu( new QMenu( this ) ) , mExpressionContextScope( tr( "Map Canvas" ) ) { mScene = new QGraphicsScene(); @@ -795,6 +799,85 @@ void QgsMapCanvas::clearTemporalCache() } } +void QgsMapCanvas::showContextMenu( QgsMapMouseEvent *event ) +{ + const QgsPointXY mapPoint = event->originalMapPoint(); + + mMenu->clear(); + + QMenu *copyCoordinateMenu = new QMenu( tr( "Copy Coordinate" ), mMenu ); + copyCoordinateMenu->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionEditCopy.svg" ) ) ); + + auto addCoordinateFormat = [ = ]( const QString identifier, const QgsCoordinateReferenceSystem & crs ) + { + QgsCoordinateTransform ct( mSettings.destinationCrs(), crs, mSettings.transformContext() ); + try + { + const QgsPointXY transformedPoint = ct.transform( mapPoint ); + + const int displayPrecision = crs.mapUnits() == QgsUnitTypes::DistanceDegrees ? 5 : 3; + QAction *copyCoordinateAction = new QAction( QStringLiteral( "%1, %2 (%3)" ).arg( + QString::number( transformedPoint.x(), 'f', displayPrecision ), + QString::number( transformedPoint.y(), 'f', displayPrecision ), + identifier ), mMenu ); + + connect( copyCoordinateAction, &QAction::triggered, this, [displayPrecision, transformedPoint] + { + QClipboard *clipboard = QApplication::clipboard(); + + const QString coordinates = QString::number( transformedPoint.x(), 'f', displayPrecision ) + ',' + QString::number( transformedPoint.y(), 'f', displayPrecision ); + + //if we are on x11 system put text into selection ready for middle button pasting + if ( clipboard->supportsSelection() ) + { + clipboard->setText( coordinates, QClipboard::Selection ); + } + clipboard->setText( coordinates, QClipboard::Clipboard ); + + } ); + copyCoordinateMenu->addAction( copyCoordinateAction ); + } + catch ( QgsCsException & ) + { + + } + }; + + addCoordinateFormat( tr( "%1 - Map CRS" ).arg( mSettings.destinationCrs().userFriendlyIdentifier( QgsCoordinateReferenceSystem::ShortString ) ), mSettings.destinationCrs() ); + if ( mSettings.destinationCrs() != QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ) + addCoordinateFormat( tr( "WGS84" ), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); + + QgsSettings settings; + const QString customCrsString = settings.value( QStringLiteral( "qgis/custom_coordinate_crs" ) ).toString(); + if ( !customCrsString.isEmpty() ) + { + QgsCoordinateReferenceSystem customCrs( customCrsString ); + if ( customCrs != mSettings.destinationCrs() && customCrs != QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ) + { + addCoordinateFormat( tr( "%1 - Custom CRS" ).arg( customCrs.userFriendlyIdentifier( QgsCoordinateReferenceSystem::ShortString ) ), customCrs ); + } + } + copyCoordinateMenu->addSeparator(); + QAction *setCustomCrsAction = new QAction( QStringLiteral( "Set Custom CRS…" ), mMenu ); + connect( setCustomCrsAction, &QAction::triggered, this, [ = ] + { + QgsProjectionSelectionDialog selector( this ); + selector.setCrs( QgsCoordinateReferenceSystem( customCrsString ) ); + if ( selector.exec() ) + { + QgsSettings().setValue( QStringLiteral( "qgis/custom_coordinate_crs" ), selector.crs().authid().isEmpty() ? selector.crs().toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED ) : selector.crs().authid() ); + } + } ); + copyCoordinateMenu->addAction( setCustomCrsAction ); + + mMenu->addMenu( copyCoordinateMenu ); + + if ( mMapTool ) + mMapTool->populateContextMenu( mMenu ); + + mMenu->exec( event->globalPos() ); +} + void QgsMapCanvas::setTemporalRange( const QgsDateTimeRange &dateTimeRange ) { if ( temporalRange() == dateTimeRange ) @@ -1601,6 +1684,12 @@ void QgsMapCanvas::mousePressEvent( QMouseEvent *e ) beginZoomRect( e->pos() ); return; } + else if ( mMapTool->flags() & QgsMapTool::ShowContextMenu && e->button() == Qt::RightButton ) + { + std::unique_ptr me( new QgsMapMouseEvent( this, e ) ); + showContextMenu( me.get() ); + return; + } else { std::unique_ptr me( new QgsMapMouseEvent( this, e ) ); @@ -1616,9 +1705,7 @@ void QgsMapCanvas::mousePressEvent( QMouseEvent *e ) mCanvasProperties->mouseButtonDown = true; mCanvasProperties->rubberStartPoint = e->pos(); - -} // mousePressEvent - +} void QgsMapCanvas::mouseReleaseEvent( QMouseEvent *e ) { @@ -1679,7 +1766,7 @@ void QgsMapCanvas::mouseReleaseEvent( QMouseEvent *e ) if ( mCanvasProperties->panSelectorDown ) return; -} // mouseReleaseEvent +} void QgsMapCanvas::resizeEvent( QResizeEvent *e ) { diff --git a/src/gui/qgsmapcanvas.h b/src/gui/qgsmapcanvas.h index 96a6f4272aad..f955801b260d 100644 --- a/src/gui/qgsmapcanvas.h +++ b/src/gui/qgsmapcanvas.h @@ -71,6 +71,9 @@ class QgsReferencedRectangle; class QgsTemporalController; +class QMenu; +class QgsMapMouseEvent; + /** * \ingroup gui @@ -1106,6 +1109,9 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView //! previous tool if current is for zooming/panning QgsMapTool *mLastNonZoomMapTool = nullptr; + //! Context menu + QMenu *mMenu = nullptr; + //! recently used extent QList mLastExtent; int mLastExtentIndex = -1; @@ -1239,6 +1245,8 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView */ void clearTemporalCache(); + void showContextMenu( QgsMapMouseEvent *event ); + friend class TestQgsMapCanvas; }; // class QgsMapCanvas diff --git a/src/gui/qgsmaptool.cpp b/src/gui/qgsmaptool.cpp index 881b2925b46f..650b1e7a4930 100644 --- a/src/gui/qgsmaptool.cpp +++ b/src/gui/qgsmaptool.cpp @@ -227,3 +227,8 @@ double QgsMapTool::searchRadiusMU( QgsMapCanvas *canvas ) QgsRenderContext context = QgsRenderContext::fromMapSettings( mapSettings ); return searchRadiusMU( context ); } + +void QgsMapTool::populateContextMenu( QMenu * ) +{ + +} diff --git a/src/gui/qgsmaptool.h b/src/gui/qgsmaptool.h index 439508277360..985cf749cdb8 100644 --- a/src/gui/qgsmaptool.h +++ b/src/gui/qgsmaptool.h @@ -39,6 +39,7 @@ class QPoint; class QAction; class QAbstractButton; class QgsMapMouseEvent; +class QMenu; #ifdef SIP_RUN % ModuleHeaderCode @@ -92,6 +93,7 @@ class GUI_EXPORT QgsMapTool : public QObject tool automatically restored. */ EditTool = 1 << 2, //!< Map tool is an edit tool, which can only be used when layer is editable AllowZoomRect = 1 << 3, //!< Allow zooming by rectangle (by holding shift and dragging) while the tool is active + ShowContextMenu = 1 << 4, //!< Show a context menu when right-clicking with the tool (since QGIS 3.14). See populateContextMenu(). }; Q_DECLARE_FLAGS( Flags, Flag ) @@ -191,6 +193,22 @@ class GUI_EXPORT QgsMapTool : public QObject * \since QGIS 2.3 */ static double searchRadiusMU( QgsMapCanvas *canvas ); + /** + * Allows the tool to populate and customize the given \a menu, + * prior to showing it in response to a right-mouse button click. + * + * \a menu will be initially populated with a set of default, generic actions. + * Any new actions added to the menu should be correctly parented to \a menu. + * + * The default implementation does nothing. + * + * \note The context menu is only shown when the ShowContextMenu flag + * is present in flags(). + * + * \since QGIS 3.14 + */ + virtual void populateContextMenu( QMenu *menu ); + signals: //! emit a message void messageEmitted( const QString &message, Qgis::MessageLevel = Qgis::Info ); diff --git a/src/gui/qgsmaptoolpan.cpp b/src/gui/qgsmaptoolpan.cpp index b8cee6710781..4180ac0d2f03 100644 --- a/src/gui/qgsmaptoolpan.cpp +++ b/src/gui/qgsmaptoolpan.cpp @@ -15,7 +15,6 @@ #include #include - #include "qgsmaptoolpan.h" #include "qgsmapcanvas.h" #include "qgsmaptopixel.h" @@ -50,6 +49,11 @@ void QgsMapToolPan::deactivate() QgsMapTool::deactivate(); } +QgsMapTool::Flags QgsMapToolPan::flags() const +{ + return QgsMapTool::Transient | QgsMapTool::AllowZoomRect | QgsMapTool::ShowContextMenu; +} + void QgsMapToolPan::canvasPressEvent( QgsMapMouseEvent *e ) { if ( e->button() == Qt::LeftButton ) diff --git a/src/gui/qgsmaptoolpan.h b/src/gui/qgsmaptoolpan.h index 1b207cd6b27e..e8056f02fb22 100644 --- a/src/gui/qgsmaptoolpan.h +++ b/src/gui/qgsmaptoolpan.h @@ -41,7 +41,7 @@ class GUI_EXPORT QgsMapToolPan : public QgsMapTool void activate() override; void deactivate() override; - Flags flags() const override { return QgsMapTool::Transient | QgsMapTool::AllowZoomRect; } + Flags flags() const override; void canvasPressEvent( QgsMapMouseEvent *e ) override; void canvasMoveEvent( QgsMapMouseEvent *e ) override; void canvasReleaseEvent( QgsMapMouseEvent *e ) override; diff --git a/src/gui/qgsmaptoolzoom.cpp b/src/gui/qgsmaptoolzoom.cpp index 624049c943e9..213e5217f5c1 100644 --- a/src/gui/qgsmaptoolzoom.cpp +++ b/src/gui/qgsmaptoolzoom.cpp @@ -47,6 +47,10 @@ QgsMapToolZoom::~QgsMapToolZoom() delete mRubberBand; } +QgsMapTool::Flags QgsMapToolZoom::flags() const +{ + return QgsMapTool::Transient | QgsMapTool::ShowContextMenu; +} void QgsMapToolZoom::canvasMoveEvent( QgsMapMouseEvent *e ) { diff --git a/src/gui/qgsmaptoolzoom.h b/src/gui/qgsmaptoolzoom.h index 1f7e3dcd9143..0c9b76588c29 100644 --- a/src/gui/qgsmaptoolzoom.h +++ b/src/gui/qgsmaptoolzoom.h @@ -36,7 +36,7 @@ class GUI_EXPORT QgsMapToolZoom : public QgsMapTool QgsMapToolZoom( QgsMapCanvas *canvas, bool zoomOut ); ~QgsMapToolZoom() override; - Flags flags() const override { return QgsMapTool::Transient; } + Flags flags() const override; void canvasMoveEvent( QgsMapMouseEvent *e ) override; void canvasPressEvent( QgsMapMouseEvent *e ) override; void canvasReleaseEvent( QgsMapMouseEvent *e ) override;