diff --git a/images/images.qrc b/images/images.qrc
index 6dc35903e4f9..d348bc6e9668 100644
--- a/images/images.qrc
+++ b/images/images.qrc
@@ -693,6 +693,7 @@
themes/default/mActionPanHighlightFeature.svg
themes/default/mActionPrevious.svg
themes/default/mActionPlay.svg
+ themes/default/cadtools/circlesintersection.svg
themes/default/cadtools/construction.svg
themes/default/cadtools/delta.svg
themes/default/cadtools/floater.svg
diff --git a/images/themes/default/cadtools/circlesintersection.svg b/images/themes/default/cadtools/circlesintersection.svg
new file mode 100644
index 000000000000..3682daa54ca3
--- /dev/null
+++ b/images/themes/default/cadtools/circlesintersection.svg
@@ -0,0 +1,19 @@
+
+
diff --git a/python/PyQt6/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in b/python/PyQt6/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in
index 2103aacc0641..b3106b79dfd2 100644
--- a/python/PyQt6/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in
+++ b/python/PyQt6/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in
@@ -138,6 +138,22 @@ is equal to :py:func:`~QgsDoubleSpinBox.minimum`. Typical use is to indicate tha
virtual void stepBy( int steps );
+ signals:
+
+ void returnPressed();
+%Docstring
+Emitted when the Return or Enter key is used in the line edit.
+
+.. versionadded:: 3.40
+%End
+
+ void textEdited( const QString &text );
+%Docstring
+Emitted when the the value has been manually edited via line edit.
+
+.. versionadded:: 3.40
+%End
+
protected:
virtual void changeEvent( QEvent *event );
diff --git a/python/PyQt6/gui/auto_generated/editorwidgets/qgsspinbox.sip.in b/python/PyQt6/gui/auto_generated/editorwidgets/qgsspinbox.sip.in
index 8e3fb06ba77e..7461c9ccd83e 100644
--- a/python/PyQt6/gui/auto_generated/editorwidgets/qgsspinbox.sip.in
+++ b/python/PyQt6/gui/auto_generated/editorwidgets/qgsspinbox.sip.in
@@ -136,6 +136,22 @@ is equal to :py:func:`~QgsSpinBox.minimum`. Typical use is to indicate that this
virtual void stepBy( int steps );
+ signals:
+
+ void returnPressed();
+%Docstring
+Emitted when the Return or Enter key is used in the line edit
+
+.. versionadded:: 3.40
+%End
+
+ void textEdited( const QString &text );
+%Docstring
+Emitted when the the value has been manually edited via line edit.
+
+.. versionadded:: 3.40
+%End
+
protected:
virtual void changeEvent( QEvent *event );
diff --git a/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in b/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in
index 874873f94a2c..92574b8ea84d 100644
--- a/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in
+++ b/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in
@@ -204,13 +204,15 @@ Removes unit suffix from the constraint text.
};
- explicit QgsAdvancedDigitizingDockWidget( QgsMapCanvas *canvas, QWidget *parent = 0 );
+ explicit QgsAdvancedDigitizingDockWidget( QgsMapCanvas *canvas, QWidget *parent = 0, QgsUserInputWidget *userInputWidget = 0 );
%Docstring
Create an advanced digitizing dock widget
:param canvas: The map canvas on which the widget operates
:param parent: The parent
+:param userInputWidget: The user input widget on which tools can add widget overlays on top of the map canvas (since QGIS 3.40)
%End
+ ~QgsAdvancedDigitizingDockWidget();
bool canvasKeyPressEventFilter( QKeyEvent *e );
%Docstring
@@ -232,6 +234,35 @@ apply the CAD constraints. The will modify the position of the map event in map
%Docstring
align to segment for between line constraint.
If between line constraints are used, this will determine the angle to be locked depending on the snapped segment.
+%End
+
+ void processCanvasPressEvent( QgsMapMouseEvent *event );
+%Docstring
+Processes the canvas press ``event``.
+%End
+
+ void processCanvasMoveEvent( QgsMapMouseEvent *event );
+%Docstring
+Processes the canvas move ``event``.
+%End
+
+ void processCanvasReleaseEvent( QgsMapMouseEvent *event );
+%Docstring
+Processes the canvas release ``event``.
+%End
+
+ void setTool( QgsAdvancedDigitizingTool *tool );
+%Docstring
+Sets an advanced digitizing tool which will take over digitizing until the tool is close.
+
+.. versionadded:: 3.40
+%End
+
+ QgsAdvancedDigitizingTool *tool() const;
+%Docstring
+Returns the current advanced digitizing tool. Returns ``None`` if not set.
+
+.. versionadded:: 3.40
%End
void releaseLocks( bool releaseRepeatingLocks = true );
@@ -372,6 +403,13 @@ Returns the X value of the X soft lock. The value is NaN is the constraint isn't
double softLockY() const;
%Docstring
Returns the Y value of the Y soft lock. The value is NaN is the constraint isn't magnetized to a line
+%End
+
+ void toggleConstraintDistance();
+%Docstring
+Toggles the distance constraint.
+
+.. versionadded:: 3.40
%End
QgsPointLocator::Match mapPointMatch() const;
diff --git a/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingtools.sip.in b/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingtools.sip.in
new file mode 100644
index 000000000000..aa95923f9a44
--- /dev/null
+++ b/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingtools.sip.in
@@ -0,0 +1,104 @@
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/gui/qgsadvanceddigitizingtools.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.pl again *
+ ************************************************************************/
+
+
+
+
+
+
+class QgsAdvancedDigitizingTool : QObject
+{
+%Docstring(signature="appended")
+An abstract class for advanced digitizing tools.
+
+.. versionadded:: 3.40
+%End
+
+%TypeHeaderCode
+#include "qgsadvanceddigitizingtools.h"
+%End
+ public:
+
+ explicit QgsAdvancedDigitizingTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget );
+%Docstring
+The advanced digitizing tool constructor.
+
+:param canvas: The map canvas on which the widget operates
+:param cadDockWidget: The cadDockWidget to which the floater belongs
+%End
+
+ QgsMapCanvas *mapCanvas() const;
+%Docstring
+Returns the map canvas associated with the tool.
+%End
+
+ QgsAdvancedDigitizingDockWidget *cadDockWidget() const;
+%Docstring
+Returns the advanced digitizing widget associated with the tool.
+%End
+
+ virtual QWidget *createWidget();
+%Docstring
+Returns a widget to control the tool.
+
+.. note::
+
+ The caller gets the ownership.
+%End
+
+ virtual void paint( QPainter *painter );
+%Docstring
+Paints tool content onto the advanced digitizing canvas item.
+%End
+
+ virtual void canvasPressEvent( QgsMapMouseEvent *event );
+%Docstring
+Handles canvas press event.
+
+.. note::
+
+ To stop propagation, set the event's accepted property to ``False``.
+%End
+
+ virtual void canvasMoveEvent( QgsMapMouseEvent *event );
+%Docstring
+Handles canvas press move.
+
+.. note::
+
+ To stop propagation, set the event's accepted property to ``False``.
+%End
+
+ virtual void canvasReleaseEvent( QgsMapMouseEvent *event );
+%Docstring
+Handles canvas release event.
+
+.. note::
+
+ To stop propagation, set the event's accepted property to ``False``.
+%End
+
+ signals:
+
+ void paintRequested();
+%Docstring
+Requests a new painting event to the advanced digitizing canvas item.
+%End
+
+ protected:
+
+};
+
+
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/gui/qgsadvanceddigitizingtools.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.pl again *
+ ************************************************************************/
diff --git a/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingtoolsregistry.sip.in b/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingtoolsregistry.sip.in
new file mode 100644
index 000000000000..3f9d6284c8e4
--- /dev/null
+++ b/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingtoolsregistry.sip.in
@@ -0,0 +1,116 @@
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/gui/qgsadvanceddigitizingtoolsregistry.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.pl again *
+ ************************************************************************/
+
+
+
+
+
+class QgsAdvancedDigitizingToolAbstractMetadata
+{
+%Docstring(signature="appended")
+Stores metadata about one advanced digitizing tool class.
+
+.. versionadded:: 3.40
+%End
+
+%TypeHeaderCode
+#include "qgsadvanceddigitizingtoolsregistry.h"
+%End
+ public:
+
+ QgsAdvancedDigitizingToolAbstractMetadata( const QString &name, const QString &visibleName, const QIcon &icon = QIcon() );
+%Docstring
+Constructor for QgsAdvancedDigitizingToolAbstractMetadata with the specified tool ``name``.
+
+``visibleName`` should be set to a translated, user visible name identifying the corresponding annotation item.
+
+An optional ``icon`` can be set, which will be used by the advanced digitizing dock widget.
+%End
+
+ virtual ~QgsAdvancedDigitizingToolAbstractMetadata();
+
+ QString name() const;
+%Docstring
+Returns the tool's unique name
+%End
+
+ QString visibleName() const;
+%Docstring
+Returns the tool's translatable user-friendly name
+%End
+
+ QIcon icon() const;
+%Docstring
+Returns the tool's icon
+%End
+
+ virtual QgsAdvancedDigitizingTool *createTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget ) /Factory/;
+%Docstring
+Returns new tool of this type. Return ``None`` on error
+%End
+
+ protected:
+};
+
+
+class QgsAdvancedDigitizingToolsRegistry
+{
+%Docstring(signature="appended")
+Registry of available advanced digitizing tools.
+
+:py:class:`QgsAdvancedDigitizingToolsRegistry` is not usually directly created, but rather accessed through
+:py:func:`QgsGui.advancedDigitizingToolsRegistry()`.
+
+.. versionadded:: 3.40
+%End
+
+%TypeHeaderCode
+#include "qgsadvanceddigitizingtoolsregistry.h"
+%End
+ public:
+
+ QgsAdvancedDigitizingToolsRegistry();
+ ~QgsAdvancedDigitizingToolsRegistry();
+
+
+ void addDefaultTools();
+%Docstring
+Adds the default tools shipped in QGIS
+%End
+
+ bool addTool( QgsAdvancedDigitizingToolAbstractMetadata *toolMetaData /Transfer/ );
+%Docstring
+Adds an advanced digitizing tool (take ownership) and return ``True`` on success
+%End
+
+ bool removeTool( const QString &name );
+%Docstring
+Removes the advanced digitizing tool matching the provided ``name`` and return ``True`` on success
+%End
+
+ QgsAdvancedDigitizingToolAbstractMetadata *toolMetadata( const QString &name );
+%Docstring
+Returns the advanced digitizing tool matching the provided ``name`` or ``None`` when no match available
+%End
+
+ const QStringList toolMetadataNames() const;
+%Docstring
+Returns the list of registered tool names
+%End
+
+ private:
+ QgsAdvancedDigitizingToolsRegistry( const QgsAdvancedDigitizingToolsRegistry &rh );
+};
+
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/gui/qgsadvanceddigitizingtoolsregistry.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.pl again *
+ ************************************************************************/
diff --git a/python/PyQt6/gui/auto_generated/qgsgui.sip.in b/python/PyQt6/gui/auto_generated/qgsgui.sip.in
index 8f3a4bf91991..cec1874c1a49 100644
--- a/python/PyQt6/gui/auto_generated/qgsgui.sip.in
+++ b/python/PyQt6/gui/auto_generated/qgsgui.sip.in
@@ -77,6 +77,13 @@ Returns the global layout item GUI registry, used for registering the GUI behavi
Returns the global annotation item GUI registry, used for registering the GUI behavior of annotation items.
.. versionadded:: 3.22
+%End
+
+ static QgsAdvancedDigitizingToolsRegistry *advancedDigitizingToolsRegistry() /KeepReference/;
+%Docstring
+Returns the global advanced digitizing tools registry, used for registering advanced digitizing tools.
+
+.. versionadded:: 3.40
%End
static QgsProcessingGuiRegistry *processingGuiRegistry() /KeepReference/;
diff --git a/python/PyQt6/gui/gui_auto.sip b/python/PyQt6/gui/gui_auto.sip
index 915f89163b66..a69c84652cb2 100644
--- a/python/PyQt6/gui/gui_auto.sip
+++ b/python/PyQt6/gui/gui_auto.sip
@@ -6,6 +6,8 @@
%Include auto_generated/qgsadvanceddigitizingcanvasitem.sip
%Include auto_generated/qgsadvanceddigitizingdockwidget.sip
%Include auto_generated/qgsadvanceddigitizingfloater.sip
+%Include auto_generated/qgsadvanceddigitizingtools.sip
+%Include auto_generated/qgsadvanceddigitizingtoolsregistry.sip
%Include auto_generated/qgsaggregatetoolbutton.sip
%Include auto_generated/qgsalignmentcombobox.sip
%Include auto_generated/qgsapplicationexitblockerinterface.sip
diff --git a/python/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in b/python/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in
index 592b70c3a898..e167a3500f26 100644
--- a/python/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in
+++ b/python/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in
@@ -138,6 +138,22 @@ is equal to :py:func:`~QgsDoubleSpinBox.minimum`. Typical use is to indicate tha
virtual void stepBy( int steps );
+ signals:
+
+ void returnPressed();
+%Docstring
+Emitted when the Return or Enter key is used in the line edit.
+
+.. versionadded:: 3.40
+%End
+
+ void textEdited( const QString &text );
+%Docstring
+Emitted when the the value has been manually edited via line edit.
+
+.. versionadded:: 3.40
+%End
+
protected:
virtual void changeEvent( QEvent *event );
diff --git a/python/gui/auto_generated/editorwidgets/qgsspinbox.sip.in b/python/gui/auto_generated/editorwidgets/qgsspinbox.sip.in
index 15de6adb253f..022beaa9ad9e 100644
--- a/python/gui/auto_generated/editorwidgets/qgsspinbox.sip.in
+++ b/python/gui/auto_generated/editorwidgets/qgsspinbox.sip.in
@@ -136,6 +136,22 @@ is equal to :py:func:`~QgsSpinBox.minimum`. Typical use is to indicate that this
virtual void stepBy( int steps );
+ signals:
+
+ void returnPressed();
+%Docstring
+Emitted when the Return or Enter key is used in the line edit
+
+.. versionadded:: 3.40
+%End
+
+ void textEdited( const QString &text );
+%Docstring
+Emitted when the the value has been manually edited via line edit.
+
+.. versionadded:: 3.40
+%End
+
protected:
virtual void changeEvent( QEvent *event );
diff --git a/python/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in b/python/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in
index bb6476e19a17..5e35a45b3a59 100644
--- a/python/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in
+++ b/python/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in
@@ -204,13 +204,15 @@ Removes unit suffix from the constraint text.
};
- explicit QgsAdvancedDigitizingDockWidget( QgsMapCanvas *canvas, QWidget *parent = 0 );
+ explicit QgsAdvancedDigitizingDockWidget( QgsMapCanvas *canvas, QWidget *parent = 0, QgsUserInputWidget *userInputWidget = 0 );
%Docstring
Create an advanced digitizing dock widget
:param canvas: The map canvas on which the widget operates
:param parent: The parent
+:param userInputWidget: The user input widget on which tools can add widget overlays on top of the map canvas (since QGIS 3.40)
%End
+ ~QgsAdvancedDigitizingDockWidget();
bool canvasKeyPressEventFilter( QKeyEvent *e );
%Docstring
@@ -232,6 +234,35 @@ apply the CAD constraints. The will modify the position of the map event in map
%Docstring
align to segment for between line constraint.
If between line constraints are used, this will determine the angle to be locked depending on the snapped segment.
+%End
+
+ void processCanvasPressEvent( QgsMapMouseEvent *event );
+%Docstring
+Processes the canvas press ``event``.
+%End
+
+ void processCanvasMoveEvent( QgsMapMouseEvent *event );
+%Docstring
+Processes the canvas move ``event``.
+%End
+
+ void processCanvasReleaseEvent( QgsMapMouseEvent *event );
+%Docstring
+Processes the canvas release ``event``.
+%End
+
+ void setTool( QgsAdvancedDigitizingTool *tool );
+%Docstring
+Sets an advanced digitizing tool which will take over digitizing until the tool is close.
+
+.. versionadded:: 3.40
+%End
+
+ QgsAdvancedDigitizingTool *tool() const;
+%Docstring
+Returns the current advanced digitizing tool. Returns ``None`` if not set.
+
+.. versionadded:: 3.40
%End
void releaseLocks( bool releaseRepeatingLocks = true );
@@ -372,6 +403,13 @@ Returns the X value of the X soft lock. The value is NaN is the constraint isn't
double softLockY() const;
%Docstring
Returns the Y value of the Y soft lock. The value is NaN is the constraint isn't magnetized to a line
+%End
+
+ void toggleConstraintDistance();
+%Docstring
+Toggles the distance constraint.
+
+.. versionadded:: 3.40
%End
QgsPointLocator::Match mapPointMatch() const;
diff --git a/python/gui/auto_generated/qgsadvanceddigitizingtools.sip.in b/python/gui/auto_generated/qgsadvanceddigitizingtools.sip.in
new file mode 100644
index 000000000000..aa95923f9a44
--- /dev/null
+++ b/python/gui/auto_generated/qgsadvanceddigitizingtools.sip.in
@@ -0,0 +1,104 @@
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/gui/qgsadvanceddigitizingtools.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.pl again *
+ ************************************************************************/
+
+
+
+
+
+
+class QgsAdvancedDigitizingTool : QObject
+{
+%Docstring(signature="appended")
+An abstract class for advanced digitizing tools.
+
+.. versionadded:: 3.40
+%End
+
+%TypeHeaderCode
+#include "qgsadvanceddigitizingtools.h"
+%End
+ public:
+
+ explicit QgsAdvancedDigitizingTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget );
+%Docstring
+The advanced digitizing tool constructor.
+
+:param canvas: The map canvas on which the widget operates
+:param cadDockWidget: The cadDockWidget to which the floater belongs
+%End
+
+ QgsMapCanvas *mapCanvas() const;
+%Docstring
+Returns the map canvas associated with the tool.
+%End
+
+ QgsAdvancedDigitizingDockWidget *cadDockWidget() const;
+%Docstring
+Returns the advanced digitizing widget associated with the tool.
+%End
+
+ virtual QWidget *createWidget();
+%Docstring
+Returns a widget to control the tool.
+
+.. note::
+
+ The caller gets the ownership.
+%End
+
+ virtual void paint( QPainter *painter );
+%Docstring
+Paints tool content onto the advanced digitizing canvas item.
+%End
+
+ virtual void canvasPressEvent( QgsMapMouseEvent *event );
+%Docstring
+Handles canvas press event.
+
+.. note::
+
+ To stop propagation, set the event's accepted property to ``False``.
+%End
+
+ virtual void canvasMoveEvent( QgsMapMouseEvent *event );
+%Docstring
+Handles canvas press move.
+
+.. note::
+
+ To stop propagation, set the event's accepted property to ``False``.
+%End
+
+ virtual void canvasReleaseEvent( QgsMapMouseEvent *event );
+%Docstring
+Handles canvas release event.
+
+.. note::
+
+ To stop propagation, set the event's accepted property to ``False``.
+%End
+
+ signals:
+
+ void paintRequested();
+%Docstring
+Requests a new painting event to the advanced digitizing canvas item.
+%End
+
+ protected:
+
+};
+
+
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/gui/qgsadvanceddigitizingtools.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.pl again *
+ ************************************************************************/
diff --git a/python/gui/auto_generated/qgsadvanceddigitizingtoolsregistry.sip.in b/python/gui/auto_generated/qgsadvanceddigitizingtoolsregistry.sip.in
new file mode 100644
index 000000000000..3f9d6284c8e4
--- /dev/null
+++ b/python/gui/auto_generated/qgsadvanceddigitizingtoolsregistry.sip.in
@@ -0,0 +1,116 @@
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/gui/qgsadvanceddigitizingtoolsregistry.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.pl again *
+ ************************************************************************/
+
+
+
+
+
+class QgsAdvancedDigitizingToolAbstractMetadata
+{
+%Docstring(signature="appended")
+Stores metadata about one advanced digitizing tool class.
+
+.. versionadded:: 3.40
+%End
+
+%TypeHeaderCode
+#include "qgsadvanceddigitizingtoolsregistry.h"
+%End
+ public:
+
+ QgsAdvancedDigitizingToolAbstractMetadata( const QString &name, const QString &visibleName, const QIcon &icon = QIcon() );
+%Docstring
+Constructor for QgsAdvancedDigitizingToolAbstractMetadata with the specified tool ``name``.
+
+``visibleName`` should be set to a translated, user visible name identifying the corresponding annotation item.
+
+An optional ``icon`` can be set, which will be used by the advanced digitizing dock widget.
+%End
+
+ virtual ~QgsAdvancedDigitizingToolAbstractMetadata();
+
+ QString name() const;
+%Docstring
+Returns the tool's unique name
+%End
+
+ QString visibleName() const;
+%Docstring
+Returns the tool's translatable user-friendly name
+%End
+
+ QIcon icon() const;
+%Docstring
+Returns the tool's icon
+%End
+
+ virtual QgsAdvancedDigitizingTool *createTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget ) /Factory/;
+%Docstring
+Returns new tool of this type. Return ``None`` on error
+%End
+
+ protected:
+};
+
+
+class QgsAdvancedDigitizingToolsRegistry
+{
+%Docstring(signature="appended")
+Registry of available advanced digitizing tools.
+
+:py:class:`QgsAdvancedDigitizingToolsRegistry` is not usually directly created, but rather accessed through
+:py:func:`QgsGui.advancedDigitizingToolsRegistry()`.
+
+.. versionadded:: 3.40
+%End
+
+%TypeHeaderCode
+#include "qgsadvanceddigitizingtoolsregistry.h"
+%End
+ public:
+
+ QgsAdvancedDigitizingToolsRegistry();
+ ~QgsAdvancedDigitizingToolsRegistry();
+
+
+ void addDefaultTools();
+%Docstring
+Adds the default tools shipped in QGIS
+%End
+
+ bool addTool( QgsAdvancedDigitizingToolAbstractMetadata *toolMetaData /Transfer/ );
+%Docstring
+Adds an advanced digitizing tool (take ownership) and return ``True`` on success
+%End
+
+ bool removeTool( const QString &name );
+%Docstring
+Removes the advanced digitizing tool matching the provided ``name`` and return ``True`` on success
+%End
+
+ QgsAdvancedDigitizingToolAbstractMetadata *toolMetadata( const QString &name );
+%Docstring
+Returns the advanced digitizing tool matching the provided ``name`` or ``None`` when no match available
+%End
+
+ const QStringList toolMetadataNames() const;
+%Docstring
+Returns the list of registered tool names
+%End
+
+ private:
+ QgsAdvancedDigitizingToolsRegistry( const QgsAdvancedDigitizingToolsRegistry &rh );
+};
+
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/gui/qgsadvanceddigitizingtoolsregistry.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.pl again *
+ ************************************************************************/
diff --git a/python/gui/auto_generated/qgsgui.sip.in b/python/gui/auto_generated/qgsgui.sip.in
index 3645bd698614..b911c74bc038 100644
--- a/python/gui/auto_generated/qgsgui.sip.in
+++ b/python/gui/auto_generated/qgsgui.sip.in
@@ -77,6 +77,13 @@ Returns the global layout item GUI registry, used for registering the GUI behavi
Returns the global annotation item GUI registry, used for registering the GUI behavior of annotation items.
.. versionadded:: 3.22
+%End
+
+ static QgsAdvancedDigitizingToolsRegistry *advancedDigitizingToolsRegistry() /KeepReference/;
+%Docstring
+Returns the global advanced digitizing tools registry, used for registering advanced digitizing tools.
+
+.. versionadded:: 3.40
%End
static QgsProcessingGuiRegistry *processingGuiRegistry() /KeepReference/;
diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip
index 915f89163b66..a69c84652cb2 100644
--- a/python/gui/gui_auto.sip
+++ b/python/gui/gui_auto.sip
@@ -6,6 +6,8 @@
%Include auto_generated/qgsadvanceddigitizingcanvasitem.sip
%Include auto_generated/qgsadvanceddigitizingdockwidget.sip
%Include auto_generated/qgsadvanceddigitizingfloater.sip
+%Include auto_generated/qgsadvanceddigitizingtools.sip
+%Include auto_generated/qgsadvanceddigitizingtoolsregistry.sip
%Include auto_generated/qgsaggregatetoolbutton.sip
%Include auto_generated/qgsalignmentcombobox.sip
%Include auto_generated/qgsapplicationexitblockerinterface.sip
diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp
index 8043e196a281..54345a47ac8c 100644
--- a/src/app/qgisapp.cpp
+++ b/src/app/qgisapp.cpp
@@ -1186,7 +1186,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipBadLayers
// Advanced Digitizing dock
startProfile( tr( "Advanced digitize panel" ) );
- mAdvancedDigitizingDockWidget = new QgsAdvancedDigitizingDockWidget( mMapCanvas, this );
+ mAdvancedDigitizingDockWidget = new QgsAdvancedDigitizingDockWidget( mMapCanvas, this, mUserInputDockWidget );
mAdvancedDigitizingDockWidget->setWindowTitle( tr( "Advanced Digitizing" ) );
mAdvancedDigitizingDockWidget->setObjectName( QStringLiteral( "AdvancedDigitizingTools" ) );
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index 1481da0395f6..021fdddbeded 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -518,6 +518,8 @@ set(QGIS_GUI_SRCS
qgsadvanceddigitizingcanvasitem.cpp
qgsadvanceddigitizingdockwidget.cpp
qgsadvanceddigitizingfloater.cpp
+ qgsadvanceddigitizingtools.cpp
+ qgsadvanceddigitizingtoolsregistry.cpp
qgsaggregatetoolbutton.cpp
qgsalignmentcombobox.cpp
qgsapplicationexitblockerinterface.cpp
@@ -790,6 +792,8 @@ set(QGIS_GUI_HDRS
qgsadvanceddigitizingcanvasitem.h
qgsadvanceddigitizingdockwidget.h
qgsadvanceddigitizingfloater.h
+ qgsadvanceddigitizingtools.h
+ qgsadvanceddigitizingtoolsregistry.h
qgsaggregatetoolbutton.h
qgsalignmentcombobox.h
qgsapplicationexitblockerinterface.h
diff --git a/src/gui/editorwidgets/qgsdoublespinbox.cpp b/src/gui/editorwidgets/qgsdoublespinbox.cpp
index 71d354f8e5dc..a4446cf16194 100644
--- a/src/gui/editorwidgets/qgsdoublespinbox.cpp
+++ b/src/gui/editorwidgets/qgsdoublespinbox.cpp
@@ -40,6 +40,7 @@ QgsDoubleSpinBox::QgsDoubleSpinBox( QWidget *parent )
: QDoubleSpinBox( parent )
{
mLineEdit = new QgsSpinBoxLineEdit();
+ connect( mLineEdit, &QLineEdit::returnPressed, this, &QgsDoubleSpinBox::returnPressed );
// By default, group separator is off
setLineEdit( mLineEdit );
diff --git a/src/gui/editorwidgets/qgsdoublespinbox.h b/src/gui/editorwidgets/qgsdoublespinbox.h
index f1ad4ddf542f..7bf91175efa3 100644
--- a/src/gui/editorwidgets/qgsdoublespinbox.h
+++ b/src/gui/editorwidgets/qgsdoublespinbox.h
@@ -142,6 +142,20 @@ class GUI_EXPORT QgsDoubleSpinBox : public QDoubleSpinBox
void paintEvent( QPaintEvent *e ) override;
void stepBy( int steps ) override;
+ signals:
+
+ /**
+ * Emitted when the Return or Enter key is used in the line edit.
+ * \since QGIS 3.40
+ */
+ void returnPressed();
+
+ /**
+ * Emitted when the the value has been manually edited via line edit.
+ * \since QGIS 3.40
+ */
+ void textEdited( const QString &text );
+
protected:
void changeEvent( QEvent *event ) override;
void wheelEvent( QWheelEvent *event ) override;
diff --git a/src/gui/editorwidgets/qgsspinbox.cpp b/src/gui/editorwidgets/qgsspinbox.cpp
index 920949936553..b9f273a9b8d6 100644
--- a/src/gui/editorwidgets/qgsspinbox.cpp
+++ b/src/gui/editorwidgets/qgsspinbox.cpp
@@ -39,6 +39,8 @@ QgsSpinBox::QgsSpinBox( QWidget *parent )
: QSpinBox( parent )
{
mLineEdit = new QgsSpinBoxLineEdit();
+ connect( mLineEdit, &QLineEdit::returnPressed, this, &QgsSpinBox::returnPressed );
+ connect( mLineEdit, &QLineEdit::textEdited, this, &QgsSpinBox::textEdited );
setLineEdit( mLineEdit );
const QSize msz = minimumSizeHint();
diff --git a/src/gui/editorwidgets/qgsspinbox.h b/src/gui/editorwidgets/qgsspinbox.h
index d9fb3367b503..636215fd7dfa 100644
--- a/src/gui/editorwidgets/qgsspinbox.h
+++ b/src/gui/editorwidgets/qgsspinbox.h
@@ -141,6 +141,20 @@ class GUI_EXPORT QgsSpinBox : public QSpinBox
QValidator::State validate( QString &input, int &pos ) const override;
void stepBy( int steps ) override;
+ signals:
+
+ /**
+ * Emitted when the Return or Enter key is used in the line edit
+ * \since QGIS 3.40
+ */
+ void returnPressed();
+
+ /**
+ * Emitted when the the value has been manually edited via line edit.
+ * \since QGIS 3.40
+ */
+ void textEdited( const QString &text );
+
protected:
void changeEvent( QEvent *event ) override;
diff --git a/src/gui/qgsadvanceddigitizingcanvasitem.cpp b/src/gui/qgsadvanceddigitizingcanvasitem.cpp
index fcc9e0dc5c85..fe67e6c7ab47 100644
--- a/src/gui/qgsadvanceddigitizingcanvasitem.cpp
+++ b/src/gui/qgsadvanceddigitizingcanvasitem.cpp
@@ -59,6 +59,14 @@ void QgsAdvancedDigitizingCanvasItem::paint( QPainter *painter )
}
}
+ // Draw current tool
+ if ( QgsAdvancedDigitizingTool *tool = mAdvancedDigitizingDockWidget->tool() )
+ {
+ // if a tool is active in the dock, then delegate to that tool to handle decorating the canvas instead of using the default decorations
+ tool->paint( painter );
+ return;
+ }
+
// Use visible polygon rather than extent to properly handle rotated maps
QPolygonF mapPoly = mMapCanvas->mapSettings().visiblePolygon();
const double canvasWidth = QLineF( mapPoly[0], mapPoly[1] ).length();
diff --git a/src/gui/qgsadvanceddigitizingdockwidget.cpp b/src/gui/qgsadvanceddigitizingdockwidget.cpp
index 550ee670218b..e0ba31247b23 100644
--- a/src/gui/qgsadvanceddigitizingdockwidget.cpp
+++ b/src/gui/qgsadvanceddigitizingdockwidget.cpp
@@ -22,9 +22,11 @@
#include "qgsadvanceddigitizingdockwidget.h"
#include "qgsadvanceddigitizingfloater.h"
#include "qgsadvanceddigitizingcanvasitem.h"
+#include "qgsadvanceddigitizingtoolsregistry.h"
#include "qgsbearingnumericformat.h"
#include "qgscadutils.h"
#include "qgsexpression.h"
+#include "qgsgui.h"
#include "qgsmapcanvas.h"
#include "qgsmaptooledit.h"
#include "qgsmaptooladvanceddigitizing.h"
@@ -38,6 +40,7 @@
#include "qgsunittypes.h"
#include "qgssettingsentryimpl.h"
#include "qgssettingstree.h"
+#include "qgsuserinputwidget.h"
#include
@@ -48,9 +51,10 @@ const QgsSettingsEntryBool *QgsAdvancedDigitizingDockWidget::settingsCadShowCons
const QgsSettingsEntryBool *QgsAdvancedDigitizingDockWidget::settingsCadSnapToConstructionGuides = new QgsSettingsEntryBool( QStringLiteral( "cad-snap-to-construction-guides" ), QgsSettingsTree::sTreeDigitizing, false, tr( "Determines if points will snap to construction guides." ) ) ;
-QgsAdvancedDigitizingDockWidget::QgsAdvancedDigitizingDockWidget( QgsMapCanvas *canvas, QWidget *parent )
+QgsAdvancedDigitizingDockWidget::QgsAdvancedDigitizingDockWidget( QgsMapCanvas *canvas, QWidget *parent, QgsUserInputWidget *userInputWidget )
: QgsDockWidget( parent )
, mMapCanvas( canvas )
+ , mUserInputWidget( userInputWidget )
, mSnapIndicator( std::make_unique< QgsSnapIndicator>( canvas ) )
, mCommonAngleConstraint( QgsSettings().value( QStringLiteral( "/Cad/CommonAngle" ), 0.0 ).toDouble() )
{
@@ -183,7 +187,6 @@ QgsAdvancedDigitizingDockWidget::QgsAdvancedDigitizingDockWidget( QgsMapCanvas *
mCommonAngleActionsMenu->addMenu( snappingPriorityMenu );
}
-
for ( QList< QPair >::const_iterator it = commonAngles.constBegin(); it != commonAngles.constEnd(); ++it )
{
QAction *action = new QAction( it->second, mCommonAngleActionsMenu );
@@ -237,6 +240,26 @@ QgsAdvancedDigitizingDockWidget::QgsAdvancedDigitizingDockWidget( QgsMapCanvas *
constructionModeToolButton->setMenu( constructionSettingsMenu );
constructionModeToolButton->setObjectName( QStringLiteral( "ConstructionModeButton" ) );
+ // Tools
+ QMenu *toolsMenu = new QMenu( this );
+ connect( toolsMenu, &QMenu::aboutToShow, this, [ = ]()
+ {
+ toolsMenu->clear();
+ const QStringList toolMetadataNames = QgsGui::instance()->advancedDigitizingToolsRegistry()->toolMetadataNames();
+ for ( const QString &name : toolMetadataNames )
+ {
+ QgsAdvancedDigitizingToolAbstractMetadata *toolMetadata = QgsGui::instance()->advancedDigitizingToolsRegistry()->toolMetadata( name );
+ QAction *toolAction = new QAction( toolMetadata->icon(), toolMetadata->visibleName(), toolsMenu );
+ connect( toolAction, &QAction::triggered, this, [ = ]()
+ {
+ setTool( toolMetadata->createTool( mMapCanvas, this ) );
+ } );
+ toolsMenu->addAction( toolAction );
+ }
+ } );
+ qobject_cast< QToolButton *>( mToolbar->widgetForAction( mToolsAction ) )->setPopupMode( QToolButton::InstantPopup );
+ mToolsAction->setMenu( toolsMenu );
+
qobject_cast< QToolButton *>( mToolbar->widgetForAction( mSettingsAction ) )->setPopupMode( QToolButton::InstantPopup );
mSettingsAction->setMenu( mCommonAngleActionsMenu );
mSettingsAction->setCheckable( true );
@@ -420,6 +443,14 @@ QgsAdvancedDigitizingDockWidget::QgsAdvancedDigitizingDockWidget( QgsMapCanvas *
disable();
}
+QgsAdvancedDigitizingDockWidget::~QgsAdvancedDigitizingDockWidget()
+{
+ if ( mCurrentTool )
+ {
+ mCurrentTool->deleteLater();
+ }
+}
+
QString QgsAdvancedDigitizingDockWidget::formatCommonAngleSnapping( double angle )
{
if ( angle == 0 )
@@ -536,6 +567,7 @@ void QgsAdvancedDigitizingDockWidget::setCadEnabled( bool enabled )
mInputWidgets->setEnabled( enabled );
mFloaterAction->setEnabled( enabled );
mConstructionAction->setEnabled( enabled );
+ mToolsAction->setEnabled( enabled );
if ( !enabled )
{
@@ -545,6 +577,10 @@ void QgsAdvancedDigitizingDockWidget::setCadEnabled( bool enabled )
// will be reactivated in updateCapacities
mParallelAction->setEnabled( false );
mPerpendicularAction->setEnabled( false );
+ if ( mCurrentTool )
+ {
+ mCurrentTool->deleteLater();
+ }
}
@@ -644,6 +680,32 @@ void QgsAdvancedDigitizingDockWidget::activateCad( bool enabled )
setCadEnabled( enabled );
}
+void QgsAdvancedDigitizingDockWidget::setTool( QgsAdvancedDigitizingTool *tool )
+{
+ if ( mCurrentTool )
+ {
+ mCurrentTool->deleteLater();
+ mCurrentTool = nullptr;
+ }
+
+ mCurrentTool = tool;
+
+ if ( mCurrentTool )
+ {
+ if ( QWidget *toolWidget = mCurrentTool->createWidget() )
+ {
+ toolWidget->setParent( mUserInputWidget );
+ mUserInputWidget->addUserInputWidget( toolWidget );
+ }
+ connect( mCurrentTool.data(), &QgsAdvancedDigitizingTool::paintRequested, this, &QgsAdvancedDigitizingDockWidget::updateCadPaintItem );
+ }
+}
+
+QgsAdvancedDigitizingTool *QgsAdvancedDigitizingDockWidget::tool() const
+{
+ return mCurrentTool.data();
+}
+
void QgsAdvancedDigitizingDockWidget::betweenLineConstraintClicked( bool activated )
{
if ( !activated )
@@ -1492,6 +1554,76 @@ QList QgsAdvancedDigitizingDockWidget::snapSegmentToAllLayers( const
return segment;
}
+void QgsAdvancedDigitizingDockWidget::processCanvasPressEvent( QgsMapMouseEvent *event )
+{
+ if ( mCurrentTool )
+ {
+ mCurrentTool->canvasPressEvent( event );
+ }
+
+ if ( constructionMode() )
+ {
+ event->setAccepted( false );
+ }
+}
+
+void QgsAdvancedDigitizingDockWidget::processCanvasMoveEvent( QgsMapMouseEvent *event )
+{
+ // perpendicular/parallel constraint
+ // do a soft lock when snapping to a segment
+ alignToSegment( event, QgsAdvancedDigitizingDockWidget::CadConstraint::SoftLock );
+
+ if ( mCurrentTool )
+ {
+ mCurrentTool->canvasMoveEvent( event );
+ }
+
+ updateCadPaintItem();
+}
+
+void QgsAdvancedDigitizingDockWidget::processCanvasReleaseEvent( QgsMapMouseEvent *event )
+{
+ if ( alignToSegment( event ) )
+ {
+ event->setAccepted( false );
+ return;
+ }
+
+ if ( mCurrentTool )
+ {
+ mCurrentTool->canvasReleaseEvent( event );
+ if ( !event->isAccepted() )
+ {
+ return;
+ }
+ else
+ {
+ // update the point list
+ QgsPoint point( event->mapPoint() );
+ point.setZ( QgsMapToolEdit::defaultZValue() );
+ point.setM( QgsMapToolEdit::defaultMValue() );
+
+ if ( mLockZButton->isChecked() )
+ {
+ point.setZ( QLocale().toDouble( mZLineEdit->text() ) );
+ }
+ if ( mLockMButton->isChecked() )
+ {
+ point.setM( QLocale().toDouble( mMLineEdit->text() ) );
+ }
+ updateCurrentPoint( point );
+ }
+ }
+
+ addPoint( event->mapPoint() );
+ releaseLocks( false );
+
+ if ( constructionMode() )
+ {
+ event->setAccepted( false );
+ }
+}
+
bool QgsAdvancedDigitizingDockWidget::alignToSegment( QgsMapMouseEvent *e, CadConstraint::LockMode lockMode )
{
if ( mBetweenLineConstraint == Qgis::BetweenLineConstraint::NoConstraint )
@@ -1621,6 +1753,13 @@ void QgsAdvancedDigitizingDockWidget::setPoints( const QList &points
}
}
+void QgsAdvancedDigitizingDockWidget::toggleConstraintDistance()
+{
+ mDistanceConstraint->toggleLocked();
+ emit lockDistanceChanged( mDistanceConstraint->isLocked() );
+ emit pointChangedV2( mCadPointList.value( 0 ) );
+}
+
bool QgsAdvancedDigitizingDockWidget::eventFilter( QObject *obj, QEvent *event )
{
if ( !cadEnabled() )
@@ -1833,9 +1972,7 @@ bool QgsAdvancedDigitizingDockWidget::filterKeyPress( QKeyEvent *e )
{
if ( mCapacities.testFlag( RelativeCoordinates ) && mCapacities.testFlag( Distance ) )
{
- mDistanceConstraint->toggleLocked();
- emit lockDistanceChanged( mDistanceConstraint->isLocked() );
- emit pointChangedV2( mCadPointList.value( 0 ) );
+ toggleConstraintDistance();
e->accept();
}
}
@@ -2001,7 +2138,6 @@ void QgsAdvancedDigitizingDockWidget::clearLockedSnapVertices( bool force )
mLockedSnapVertices.clear();
}
-
void QgsAdvancedDigitizingDockWidget::addPoint( const QgsPointXY &point )
{
QgsPoint pt = pointXYToPoint( point );
diff --git a/src/gui/qgsadvanceddigitizingdockwidget.h b/src/gui/qgsadvanceddigitizingdockwidget.h
index 13d3d7ecc3ed..5f501c56106b 100644
--- a/src/gui/qgsadvanceddigitizingdockwidget.h
+++ b/src/gui/qgsadvanceddigitizingdockwidget.h
@@ -24,6 +24,7 @@
#include "ui_qgsadvanceddigitizingdockwidgetbase.h"
#include "qgis_gui.h"
#include "qgis_sip.h"
+#include "qgsadvanceddigitizingtools.h"
#include "qgsdockwidget.h"
#include "qgsmessagebaritem.h"
#include "qgspointxy.h"
@@ -33,10 +34,12 @@
class QgsAdvancedDigitizingCanvasItem;
class QgsAdvancedDigitizingFloater;
+class QgsAdvancedDigitizingTool;
class QgsMapCanvas;
class QgsMapTool;
class QgsMapToolAdvancedDigitizing;
class QgsMapMouseEvent;
+class QgsUserInputWidget;
/**
* \ingroup gui
@@ -248,8 +251,10 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private
* Create an advanced digitizing dock widget
* \param canvas The map canvas on which the widget operates
* \param parent The parent
+ * \param userInputWidget The user input widget on which tools can add widget overlays on top of the map canvas (since QGIS 3.40)
*/
- explicit QgsAdvancedDigitizingDockWidget( QgsMapCanvas *canvas, QWidget *parent = nullptr );
+ explicit QgsAdvancedDigitizingDockWidget( QgsMapCanvas *canvas, QWidget *parent = nullptr, QgsUserInputWidget *userInputWidget = nullptr );
+ ~QgsAdvancedDigitizingDockWidget();
/**
* Filter key events to e.g. toggle construction mode or adapt constraints
@@ -271,6 +276,33 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private
*/
bool alignToSegment( QgsMapMouseEvent *e, QgsAdvancedDigitizingDockWidget::CadConstraint::LockMode lockMode = QgsAdvancedDigitizingDockWidget::CadConstraint::HardLock );
+ /**
+ * Processes the canvas press \a event.
+ */
+ void processCanvasPressEvent( QgsMapMouseEvent *event );
+
+ /**
+ * Processes the canvas move \a event.
+ */
+ void processCanvasMoveEvent( QgsMapMouseEvent *event );
+
+ /**
+ * Processes the canvas release \a event.
+ */
+ void processCanvasReleaseEvent( QgsMapMouseEvent *event );
+
+ /**
+ * Sets an advanced digitizing tool which will take over digitizing until the tool is close.
+ * \since QGIS 3.40
+ */
+ void setTool( QgsAdvancedDigitizingTool *tool );
+
+ /**
+ * Returns the current advanced digitizing tool. Returns NULLPTR if not set.
+ * \since QGIS 3.40
+ */
+ QgsAdvancedDigitizingTool *tool() const;
+
/**
* unlock all constraints
* \param releaseRepeatingLocks set to FALSE to preserve the lock for any constraints set to repeating lock mode
@@ -378,6 +410,12 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private
//! Returns the Y value of the Y soft lock. The value is NaN is the constraint isn't magnetized to a line
double softLockY() const { return mSoftLockY; }
+ /**
+ * Toggles the distance constraint.
+ * \since QGIS 3.40
+ */
+ void toggleConstraintDistance();
+
/**
* Returns the point locator match
* \since QGIS 3.4
@@ -1059,6 +1097,8 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private
void updateConstructionGuidesCrs();
QgsMapCanvas *mMapCanvas = nullptr;
+ QgsUserInputWidget *mUserInputWidget = nullptr;
+
QgsAdvancedDigitizingCanvasItem *mCadPaintItem = nullptr;
//! Snapping indicator
std::unique_ptr mSnapIndicator;
@@ -1124,6 +1164,9 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private
double mSoftLockY;
QQueue< QgsPointLocator::Match > mLockedSnapVertices;
+ // Advanced digitizing tool
+ QPointer mCurrentTool;
+
#ifdef SIP_RUN
//! event filter for line edits in the dock UI (angle/distance/x/y line edits)
bool eventFilter( QObject *obj, QEvent *event );
diff --git a/src/gui/qgsadvanceddigitizingtools.cpp b/src/gui/qgsadvanceddigitizingtools.cpp
new file mode 100644
index 000000000000..7f9bdef73bba
--- /dev/null
+++ b/src/gui/qgsadvanceddigitizingtools.cpp
@@ -0,0 +1,299 @@
+/***************************************************************************
+ qgsadvanceddigitizingintersectiontools.cpp
+ ----------------------
+ begin : May 2024
+ copyright : (C) Mathieu Pellerin
+ email : mathieu 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
+#include
+#include
+
+#include "qgsadvanceddigitizingtools.h"
+#include "qgsapplication.h"
+#include "qgsdoublespinbox.h"
+#include "qgsmapcanvas.h"
+
+QgsAdvancedDigitizingTool::QgsAdvancedDigitizingTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget )
+ : QObject( canvas ? canvas->viewport() : nullptr )
+ , mMapCanvas( canvas )
+ , mCadDockWidget( cadDockWidget )
+{
+}
+
+QgsAdvancedDigitizingCirclesIntersectionTool::QgsAdvancedDigitizingCirclesIntersectionTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget )
+ : QgsAdvancedDigitizingTool( canvas, cadDockWidget )
+{
+}
+
+QgsAdvancedDigitizingCirclesIntersectionTool::~QgsAdvancedDigitizingCirclesIntersectionTool()
+{
+ if ( mToolWidget )
+ {
+ mToolWidget->deleteLater();
+ }
+}
+
+QWidget *QgsAdvancedDigitizingCirclesIntersectionTool::createWidget()
+{
+ QWidget *toolWidget = new QWidget();
+
+ QGridLayout *layout = new QGridLayout( toolWidget );
+ layout->setContentsMargins( 0, 0, 0, 0 );
+ toolWidget->setLayout( layout );
+
+ QLabel *label = new QLabel( QStringLiteral( "Circle #1" ), toolWidget );
+ layout->addWidget( label, 0, 0, 1, 3 );
+
+ mCircle1Digitize = new QToolButton( toolWidget );
+ mCircle1Digitize->setCheckable( true );
+ mCircle1Digitize->setChecked( false );
+ mCircle1Digitize->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionMapIdentification.svg" ) ) );
+ connect( mCircle1Digitize, &QAbstractButton::toggled, this, [ = ]( bool checked )
+ {
+ if ( checked )
+ {
+ mCircle2Digitize->setChecked( false );
+ }
+ } );
+ layout->addWidget( mCircle1Digitize, 1, 2, 2, 1 );
+
+ label = new QLabel( QStringLiteral( "X" ), toolWidget );
+ layout->addWidget( label, 1, 0 );
+
+ mCircle1X = new QgsDoubleSpinBox( toolWidget );
+ mCircle1X->setMinimum( std::numeric_limits::min() );
+ mCircle1X->setMaximum( std::numeric_limits::max() );
+ connect( mCircle1X, &QgsDoubleSpinBox::textEdited, this, [ = ]() { mCircle1Digitize->setChecked( false ); } );
+ layout->addWidget( mCircle1X, 1, 1 );
+
+ label = new QLabel( QStringLiteral( "Y" ), toolWidget );
+ layout->addWidget( label, 2, 0 );
+
+ mCircle1Y = new QgsDoubleSpinBox( toolWidget );
+ mCircle1Y->setMinimum( std::numeric_limits::min() );
+ mCircle1Y->setMaximum( std::numeric_limits::max() );
+ connect( mCircle1Y, &QgsDoubleSpinBox::textEdited, this, [ = ]() { mCircle1Digitize->setChecked( false ); } );
+ layout->addWidget( mCircle1Y, 2, 1 );
+
+ label = new QLabel( QStringLiteral( "Distance" ), toolWidget );
+ layout->addWidget( label, 3, 0 );
+
+ mCircle1Distance = new QgsDoubleSpinBox( toolWidget );
+ mCircle1Distance->setMinimum( 0 );
+ mCircle1Distance->setMaximum( std::numeric_limits::max() );
+ connect( mCircle1Distance, &QgsDoubleSpinBox::returnPressed, this, [ = ]() { mCircle2Digitize->setChecked( true ); } );
+ layout->addWidget( mCircle1Distance, 3, 1 );
+
+ label = new QLabel( QStringLiteral( "Circle #2" ), toolWidget );
+ layout->addWidget( label, 4, 0, 1, 3 );
+
+ mCircle2Digitize = new QToolButton( toolWidget );
+ mCircle2Digitize->setCheckable( true );
+ mCircle2Digitize->setChecked( false );
+ mCircle2Digitize->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionMapIdentification.svg" ) ) );
+ connect( mCircle2Digitize, &QAbstractButton::toggled, this, [ = ]( bool checked )
+ {
+ if ( checked )
+ {
+ mCircle1Digitize->setChecked( false );
+ }
+ } );
+ layout->addWidget( mCircle2Digitize, 5, 2, 2, 1 );
+
+ label = new QLabel( QStringLiteral( "X" ), toolWidget );
+ layout->addWidget( label, 5, 0 );
+
+ mCircle2X = new QgsDoubleSpinBox( toolWidget );
+ mCircle2X->setMinimum( std::numeric_limits::min() );
+ mCircle2X->setMaximum( std::numeric_limits::max() );
+ connect( mCircle2X, &QgsDoubleSpinBox::textEdited, this, [ = ]() { mCircle2Digitize->setChecked( false ); } );
+ layout->addWidget( mCircle2X, 5, 1 );
+
+ label = new QLabel( QStringLiteral( "Y" ), toolWidget );
+ layout->addWidget( label, 6, 0 );
+
+ mCircle2Y = new QgsDoubleSpinBox( toolWidget );
+ mCircle2Y->setMinimum( std::numeric_limits::min() );
+ mCircle2Y->setMaximum( std::numeric_limits::max() );
+ connect( mCircle2Y, &QgsDoubleSpinBox::textEdited, this, [ = ]() { mCircle2Digitize->setChecked( false ); } );
+ layout->addWidget( mCircle2Y, 6, 1 );
+
+ label = new QLabel( QStringLiteral( "Distance" ), toolWidget );
+ layout->addWidget( label, 7, 0 );
+
+ mCircle2Distance = new QgsDoubleSpinBox( toolWidget );
+ mCircle2Distance->setMinimum( 0 );
+ mCircle2Distance->setMaximum( std::numeric_limits::max() );
+ layout->addWidget( mCircle2Distance, 7, 1 );
+
+ connect( mCircle1X, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, [ = ]( double ) { processParameters(); } );
+ connect( mCircle1Y, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, [ = ]( double ) { processParameters(); } );
+ connect( mCircle1Distance, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, [ = ]( double ) { processParameters(); } );
+ connect( mCircle2X, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, [ = ]( double ) { processParameters(); } );
+ connect( mCircle2Y, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, [ = ]( double ) { processParameters(); } );
+ connect( mCircle2Distance, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, [ = ]( double ) { processParameters(); } );
+
+
+ bool focusOnCircle2 = false;
+ if ( mCadDockWidget )
+ {
+ if ( mCadDockWidget->constraintDistance()->isLocked() )
+ {
+ QgsPoint point = mCadDockWidget->previousPointV2();
+ if ( !point.isEmpty() )
+ {
+ whileBlocking( mCircle1Distance )->setValue( mCadDockWidget->constraintDistance()->value() );
+ whileBlocking( mCircle1X )->setValue( point.x() );
+ whileBlocking( mCircle1Y )->setValue( point.y() );
+ mP1 = point;
+ focusOnCircle2 = true;
+
+ mCadDockWidget->toggleConstraintDistance();
+ }
+ }
+ }
+
+ if ( focusOnCircle2 )
+ {
+ mCircle2Digitize->setChecked( true );
+ }
+ else
+ {
+ mCircle1Digitize->setChecked( true );
+ }
+
+ mToolWidget = toolWidget;
+ return toolWidget;
+}
+
+void QgsAdvancedDigitizingCirclesIntersectionTool::processParameters()
+{
+ mP1 = QgsPointXY();
+ mP2 = QgsPointXY();
+ QgsGeometryUtils::circleCircleIntersections( QgsPointXY( mCircle1X->value(), mCircle1Y->value() ), mCircle1Distance->value(),
+ QgsPointXY( mCircle2X->value(), mCircle2Y->value() ), mCircle2Distance->value(),
+ mP1, mP2 );
+ emit paintRequested();
+}
+
+void QgsAdvancedDigitizingCirclesIntersectionTool::canvasMoveEvent( QgsMapMouseEvent *event )
+{
+ if ( mCircle1Digitize->isChecked() )
+ {
+ mCircle1X->setValue( event->mapPoint().x() );
+ mCircle1Y->setValue( event->mapPoint().y() );
+ }
+ else if ( mCircle2Digitize->isChecked() )
+ {
+ mCircle2X->setValue( event->mapPoint().x() );
+ mCircle2Y->setValue( event->mapPoint().y() );
+ }
+
+ if ( !mP1.isEmpty() )
+ {
+ mP1Closest = QgsGeometryUtils::distance2D( QgsPoint( mP1 ), QgsPoint( event->mapPoint() ) ) < QgsGeometryUtils::distance2D( QgsPoint( mP2 ), QgsPoint( event->mapPoint() ) );
+ }
+
+ event->setAccepted( false );
+}
+
+void QgsAdvancedDigitizingCirclesIntersectionTool::canvasReleaseEvent( QgsMapMouseEvent *event )
+{
+ if ( mCircle1Digitize->isChecked() )
+ {
+ mCircle1X->setValue( event->mapPoint().x() );
+ mCircle1Y->setValue( event->mapPoint().y() );
+ mCircle1Digitize->setChecked( false );
+ mCircle1Distance->setFocus();
+ mCircle1Distance->selectAll();
+ event->setAccepted( false );
+ return;
+ }
+ else if ( mCircle2Digitize->isChecked() )
+ {
+ mCircle2X->setValue( event->mapPoint().x() );
+ mCircle2Y->setValue( event->mapPoint().y() );
+ mCircle2Digitize->setChecked( false );
+ mCircle2Distance->setFocus();
+ mCircle2Distance->selectAll();
+ event->setAccepted( false );
+ return;
+ }
+
+ if ( !mP1.isEmpty() )
+ {
+ mP1Closest = QgsGeometryUtils::distance2D( QgsPoint( mP1 ), QgsPoint( event->mapPoint() ) ) < QgsGeometryUtils::distance2D( QgsPoint( mP2 ), QgsPoint( event->mapPoint() ) );
+ event->setMapPoint( mP1Closest ? mP1 : mP2 );
+ if ( mToolWidget )
+ {
+ mToolWidget->deleteLater();
+ }
+ deleteLater();
+ return;
+ }
+
+ event->setAccepted( false );
+}
+
+void QgsAdvancedDigitizingCirclesIntersectionTool::drawCircle( QPainter *painter, double x, double y, double distance )
+{
+ painter->setPen( QPen( QColor( 0, 127, 0, 200 ), 2 ) );
+ painter->setBrush( Qt::NoBrush );
+
+ mapCanvas()->getCoordinateTransform()->transformInPlace( x, y );
+ painter->drawLine( QLineF( x - 3, y - 3, x + 3, y + 3 ) );
+ painter->drawLine( QLineF( x - 3, y + 3, x + 3, y - 3 ) );
+
+ painter->setPen( QPen( QColor( 0, 127, 0, 150 ), 1, Qt::DashLine ) );
+ distance = distance / mapCanvas()->mapSettings().mapUnitsPerPixel();
+ painter->drawEllipse( QRectF( x - distance, y - distance, distance * 2, distance * 2 ) );
+}
+
+void QgsAdvancedDigitizingCirclesIntersectionTool::drawCandidate( QPainter *painter, double x, double y, bool closest )
+{
+ if ( closest )
+ {
+ painter->setPen( QPen( QColor( 127, 0, 0, 222 ), 4 ) );
+ }
+ else
+ {
+ painter->setPen( QPen( QColor( 0, 127, 0, 222 ), 2 ) );
+ }
+
+ mapCanvas()->getCoordinateTransform()->transformInPlace( x, y );
+ double size = closest ? 5 : 3;
+ painter->drawRect( QRectF( x - size, y - size, size * 2, size * 2 ) );
+}
+
+void QgsAdvancedDigitizingCirclesIntersectionTool::paint( QPainter *painter )
+{
+ painter->save();
+
+ drawCircle( painter, mCircle1X->value(), mCircle1Y->value(), mCircle1Distance->value() );
+ drawCircle( painter, mCircle2X->value(), mCircle2Y->value(), mCircle2Distance->value() );
+
+ if ( !mP1.isEmpty() )
+ {
+ if ( mP1Closest )
+ {
+ drawCandidate( painter, mP2.x(), mP2.y(), false );
+ drawCandidate( painter, mP1.x(), mP1.y(), true );
+ }
+ else
+ {
+ drawCandidate( painter, mP1.x(), mP1.y(), false );
+ drawCandidate( painter, mP2.x(), mP2.y(), true );
+ }
+ }
+
+ painter->restore();
+}
diff --git a/src/gui/qgsadvanceddigitizingtools.h b/src/gui/qgsadvanceddigitizingtools.h
new file mode 100644
index 000000000000..fa3a83467a0f
--- /dev/null
+++ b/src/gui/qgsadvanceddigitizingtools.h
@@ -0,0 +1,164 @@
+/***************************************************************************
+ qgsadvanceddigitizingtools.h
+ ----------------------
+ begin : May 2024
+ copyright : (C) Mathieu Pellerin
+ email : mathieu 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 QGSADVANCEDDIGITIZINGTOOLS
+#define QGSADVANCEDDIGITIZINGTOOLS
+
+#include
+#include
+
+#include "qgsadvanceddigitizingdockwidget.h"
+#include "qgis_gui.h"
+#include "qgis_sip.h"
+#include "qgsmapmouseevent.h"
+
+#include
+
+class QgsAdvancedDigitizingDockWidget;
+class QgsDoubleSpinBox;
+class QgsMapCanvas;
+
+/**
+ * \ingroup gui
+ * \brief An abstract class for advanced digitizing tools.
+ * \since QGIS 3.40
+ */
+class GUI_EXPORT QgsAdvancedDigitizingTool : public QObject
+{
+ Q_OBJECT
+
+ public:
+
+ /**
+ * The advanced digitizing tool constructor.
+ * \param canvas The map canvas on which the widget operates
+ * \param cadDockWidget The cadDockWidget to which the floater belongs
+ */
+ explicit QgsAdvancedDigitizingTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget );
+
+ /**
+ * Returns the map canvas associated with the tool.
+ */
+ QgsMapCanvas *mapCanvas() const { return mMapCanvas; }
+
+ /**
+ * Returns the advanced digitizing widget associated with the tool.
+ */
+ QgsAdvancedDigitizingDockWidget *cadDockWidget() const { return mCadDockWidget.data(); }
+
+ /**
+ * Returns a widget to control the tool.
+ * \note The caller gets the ownership.
+ */
+ virtual QWidget *createWidget() { return nullptr; }
+
+ /**
+ * Paints tool content onto the advanced digitizing canvas item.
+ */
+ virtual void paint( QPainter *painter ) { Q_UNUSED( painter ) }
+
+ /**
+ * Handles canvas press event.
+ * \note To stop propagation, set the event's accepted property to FALSE.
+ */
+ virtual void canvasPressEvent( QgsMapMouseEvent *event )
+ {
+ Q_UNUSED( event )
+ }
+
+ /**
+ * Handles canvas press move.
+ * \note To stop propagation, set the event's accepted property to FALSE.
+ */
+ virtual void canvasMoveEvent( QgsMapMouseEvent *event )
+ {
+ Q_UNUSED( event )
+ }
+
+ /**
+ * Handles canvas release event.
+ * \note To stop propagation, set the event's accepted property to FALSE.
+ */
+ virtual void canvasReleaseEvent( QgsMapMouseEvent *event )
+ {
+ Q_UNUSED( event )
+ }
+
+ signals:
+
+ /**
+ * Requests a new painting event to the advanced digitizing canvas item.
+ */
+ void paintRequested();
+
+ protected:
+
+ QgsMapCanvas *mMapCanvas = nullptr;
+ QPointer< QgsAdvancedDigitizingDockWidget > mCadDockWidget;
+};
+
+#ifndef SIP_RUN
+
+/**
+ * \ingroup gui
+ * \brief A advanced digitizing tools to handle the selection of a point at the intersection
+ * of two circles.
+ * \since QGIS 3.40
+ */
+class GUI_EXPORT QgsAdvancedDigitizingCirclesIntersectionTool : public QgsAdvancedDigitizingTool
+{
+ Q_OBJECT
+
+ public:
+
+ /**
+ * The advanced digitizing's circles intersection tool constructor.
+ * \param canvas The map canvas on which the widget operates
+ * \param cadDockWidget The cadDockWidget to which the floater belongs
+ */
+ explicit QgsAdvancedDigitizingCirclesIntersectionTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget );
+ ~QgsAdvancedDigitizingCirclesIntersectionTool();
+
+ QWidget *createWidget() override;
+ void paint( QPainter *painter ) override;
+
+ void canvasMoveEvent( QgsMapMouseEvent *event ) override;
+ void canvasReleaseEvent( QgsMapMouseEvent *event ) override;
+
+ private:
+ void processParameters();
+
+ void drawCircle( QPainter *painter, double x, double y, double distance );
+ void drawCandidate( QPainter *painter, double x, double y, bool closest );
+
+ QPointer mToolWidget;
+ QToolButton *mCircle1Digitize = nullptr;
+ QgsDoubleSpinBox *mCircle1X = nullptr;
+ QgsDoubleSpinBox *mCircle1Y = nullptr;
+ QgsDoubleSpinBox *mCircle1Distance = nullptr;
+
+ QToolButton *mCircle2Digitize = nullptr;
+ QgsDoubleSpinBox *mCircle2X = nullptr;
+ QgsDoubleSpinBox *mCircle2Y = nullptr;
+ QgsDoubleSpinBox *mCircle2Distance = nullptr;
+
+ QgsPointXY mP1;
+ QgsPointXY mP2;
+ bool mP1Closest = false;
+};
+
+#endif
+
+#endif // QGSADVANCEDDIGITIZINGTOOLS
diff --git a/src/gui/qgsadvanceddigitizingtoolsregistry.cpp b/src/gui/qgsadvanceddigitizingtoolsregistry.cpp
new file mode 100644
index 000000000000..6f1b09a75f4b
--- /dev/null
+++ b/src/gui/qgsadvanceddigitizingtoolsregistry.cpp
@@ -0,0 +1,78 @@
+/***************************************************************************
+ qgsadvanceddigitizingtoolsregistry.cpp
+ -------------------
+ begin : July 27 2024
+ copyright : (C) 2024 by Mathieu Pellerin
+ email : mathieu 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 "qgsadvanceddigitizingtoolsregistry.h"
+#include "qgsadvanceddigitizingtools.h"
+#include "qgsapplication.h"
+
+QgsAdvancedDigitizingTool *QgsAdvancedDigitizingToolAbstractMetadata::createTool( QgsMapCanvas *, QgsAdvancedDigitizingDockWidget * )
+{
+ return nullptr;
+}
+
+QgsAdvancedDigitizingTool *QgsAdvancedDigitizingToolMetadata::createTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget )
+{
+ return mToolFunc ? mToolFunc( canvas, cadDockWidget ) : QgsAdvancedDigitizingToolAbstractMetadata::createTool( canvas, cadDockWidget );
+}
+
+QgsAdvancedDigitizingToolsRegistry::~QgsAdvancedDigitizingToolsRegistry()
+{
+ qDeleteAll( mTools );
+}
+
+void QgsAdvancedDigitizingToolsRegistry::addDefaultTools()
+{
+ addTool( new QgsAdvancedDigitizingToolMetadata( QStringLiteral( "circlesintersection" ),
+ QObject::tr( "2-Circle Point Intersection" ),
+ QgsApplication::getThemeIcon( QStringLiteral( "/cadtools/circlesintersection.svg" ) ),
+ [ = ]( QgsMapCanvas * canvas, QgsAdvancedDigitizingDockWidget * cadDockWidget )->QgsAdvancedDigitizingTool *
+ {
+ return new QgsAdvancedDigitizingCirclesIntersectionTool( canvas, cadDockWidget );
+ } ) );
+}
+
+bool QgsAdvancedDigitizingToolsRegistry::addTool( QgsAdvancedDigitizingToolAbstractMetadata *toolMetaData )
+{
+ if ( mTools.contains( toolMetaData->name() ) )
+ return false;
+
+ mTools[toolMetaData->name()] = toolMetaData;
+
+ return true;
+}
+
+
+bool QgsAdvancedDigitizingToolsRegistry::removeTool( const QString &name )
+{
+ if ( !mTools.contains( name ) )
+ return false;
+
+ delete mTools.take( name );
+
+ return true;
+}
+
+QgsAdvancedDigitizingToolAbstractMetadata *QgsAdvancedDigitizingToolsRegistry::toolMetadata( const QString &name )
+{
+ if ( !mTools.contains( name ) )
+ return nullptr;
+
+ return mTools[name];
+}
+
+const QStringList QgsAdvancedDigitizingToolsRegistry::toolMetadataNames() const
+{
+ return mTools.keys();
+}
diff --git a/src/gui/qgsadvanceddigitizingtoolsregistry.h b/src/gui/qgsadvanceddigitizingtoolsregistry.h
new file mode 100644
index 000000000000..e320822053d3
--- /dev/null
+++ b/src/gui/qgsadvanceddigitizingtoolsregistry.h
@@ -0,0 +1,156 @@
+/***************************************************************************
+ qgsadvanceddigitizingtoolsregistry.cpp
+ -------------------
+ begin : July 27 2024
+ copyright : (C) 2024 by Mathieu Pellerin
+ email : mathieu 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 QGSADVANCEDDIGITIZINGTOOLSREGSITRY_H
+#define QGSADVANCEDDIGITIZINGTOOLSREGSITRY_H
+
+#include "qgis_sip.h"
+#include "qgis_gui.h"
+
+#include
+#include
+
+class QgsAdvancedDigitizingDockWidget;
+class QgsAdvancedDigitizingTool;
+class QgsMapCanvas;
+
+/**
+ * \ingroup gui
+ * \brief Stores metadata about one advanced digitizing tool class.
+ * \since QGIS 3.40
+ */
+class GUI_EXPORT QgsAdvancedDigitizingToolAbstractMetadata
+{
+ public:
+
+ /**
+ * Constructor for QgsAdvancedDigitizingToolAbstractMetadata with the specified tool \a name.
+ *
+ * \a visibleName should be set to a translated, user visible name identifying the corresponding annotation item.
+ *
+ * An optional \a icon can be set, which will be used by the advanced digitizing dock widget.
+ */
+ QgsAdvancedDigitizingToolAbstractMetadata( const QString &name, const QString &visibleName, const QIcon &icon = QIcon() )
+ : mName( name )
+ , mVisibleName( visibleName )
+ , mIcon( icon )
+ {}
+
+ virtual ~QgsAdvancedDigitizingToolAbstractMetadata() = default;
+
+ //! Returns the tool's unique name
+ QString name() const { return mName; }
+
+ //! Returns the tool's translatable user-friendly name
+ QString visibleName() const { return mVisibleName; }
+
+ //! Returns the tool's icon
+ QIcon icon() const { return mIcon; }
+
+ //! Returns new tool of this type. Return NULLPTR on error
+ virtual QgsAdvancedDigitizingTool *createTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget ) SIP_FACTORY;
+
+ protected:
+ QString mName;
+ QString mVisibleName;
+ QIcon mIcon;
+};
+
+#ifndef SIP_RUN
+
+typedef std::function QgsAdvancedDigitizingToolFunc SIP_SKIP;
+
+/**
+ * \ingroup gui
+ * \brief Convenience metadata class that uses static functions to handle advanced digitizing tool creation
+ * \note not available in Python bindings
+ * \since QGIS 3.40
+ */
+class GUI_EXPORT QgsAdvancedDigitizingToolMetadata : public QgsAdvancedDigitizingToolAbstractMetadata
+{
+ public:
+
+ /**
+ * Constructor for QgsAdvancedDigitizingToolAbstractMetadata with the specified tool \a name.
+ *
+ * \a visibleName should be set to a translated, user visible name identifying the corresponding annotation item.
+ *
+ * An optional \a icon can be set, which will be used by the advanced digitizing dock widget.
+ *
+ * A tool creation function can be declared through the \a toolFunction parameter.
+ */
+ QgsAdvancedDigitizingToolMetadata( const QString &name, const QString &visibleName, const QIcon &icon = QIcon(),
+ const QgsAdvancedDigitizingToolFunc &toolFunction = nullptr )
+ : QgsAdvancedDigitizingToolAbstractMetadata( name, visibleName, icon )
+ , mToolFunc( toolFunction )
+ {}
+
+ //! Returns the tool creation function
+ QgsAdvancedDigitizingToolFunc toolFunction() const { return mToolFunc; }
+
+ //! Sets the tool creation \a function.
+ void setToolFunction( const QgsAdvancedDigitizingToolFunc &function ) { mToolFunc = function; }
+
+ QgsAdvancedDigitizingTool *createTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget ) override;
+
+ protected:
+ QgsAdvancedDigitizingToolFunc mToolFunc = nullptr;
+};
+
+#endif
+
+/**
+ * \ingroup gui
+ * \brief Registry of available advanced digitizing tools.
+ *
+ * QgsAdvancedDigitizingToolsRegistry is not usually directly created, but rather accessed through
+ * QgsGui::advancedDigitizingToolsRegistry().
+ *
+ * \since QGIS 3.40
+ */
+class GUI_EXPORT QgsAdvancedDigitizingToolsRegistry
+{
+ public:
+
+ QgsAdvancedDigitizingToolsRegistry() = default;
+ ~QgsAdvancedDigitizingToolsRegistry();
+
+ QgsAdvancedDigitizingToolsRegistry( const QgsAdvancedDigitizingToolsRegistry &rh ) = delete;
+ QgsAdvancedDigitizingToolsRegistry &operator=( const QgsAdvancedDigitizingToolsRegistry &rh ) = delete;
+
+ //! Adds the default tools shipped in QGIS
+ void addDefaultTools();
+
+ //! Adds an advanced digitizing tool (take ownership) and return TRUE on success
+ bool addTool( QgsAdvancedDigitizingToolAbstractMetadata *toolMetaData SIP_TRANSFER );
+
+ //! Removes the advanced digitizing tool matching the provided \a name and return TRUE on success
+ bool removeTool( const QString &name );
+
+ //! Returns the advanced digitizing tool matching the provided \a name or NULLPTR when no match available
+ QgsAdvancedDigitizingToolAbstractMetadata *toolMetadata( const QString &name );
+
+ //! Returns the list of registered tool names
+ const QStringList toolMetadataNames() const;
+
+ private:
+#ifdef SIP_RUN
+ QgsAdvancedDigitizingToolsRegistry( const QgsAdvancedDigitizingToolsRegistry &rh );
+#endif
+
+ QMap mTools;
+};
+
+#endif // QGSADVANCEDDIGITIZINGTOOLSREGSITRY_H
diff --git a/src/gui/qgsgui.cpp b/src/gui/qgsgui.cpp
index 86404cfb4802..7daacba99b1b 100644
--- a/src/gui/qgsgui.cpp
+++ b/src/gui/qgsgui.cpp
@@ -26,6 +26,7 @@
#include "qgssourceselectproviderregistry.h"
#include "qgslayoutitemguiregistry.h"
#include "qgsannotationitemguiregistry.h"
+#include "qgsadvanceddigitizingtoolsregistry.h"
#include "qgscalloutsregistry.h"
#include "callouts/qgscalloutwidget.h"
#ifdef Q_OS_MACX
@@ -144,6 +145,11 @@ QgsAnnotationItemGuiRegistry *QgsGui::annotationItemGuiRegistry()
return instance()->mAnnotationItemGuiRegistry;
}
+QgsAdvancedDigitizingToolsRegistry *QgsGui::advancedDigitizingToolsRegistry()
+{
+ return instance()->mAdvancedDigitizingToolsRegistry;
+}
+
QgsProcessingGuiRegistry *QgsGui::processingGuiRegistry()
{
return instance()->mProcessingGuiRegistry;
@@ -243,6 +249,7 @@ QgsGui::~QgsGui()
delete mProcessingRecentAlgorithmLog;
delete mLayoutItemGuiRegistry;
delete mAnnotationItemGuiRegistry;
+ delete mAdvancedDigitizingToolsRegistry;
delete mLayerTreeEmbeddedWidgetRegistry;
delete mEditorWidgetRegistry;
delete mMapLayerActionRegistry;
@@ -352,6 +359,9 @@ QgsGui::QgsGui()
mAnnotationItemGuiRegistry = new QgsAnnotationItemGuiRegistry();
mAnnotationItemGuiRegistry->addDefaultItems();
+ mAdvancedDigitizingToolsRegistry = new QgsAdvancedDigitizingToolsRegistry();
+ mAdvancedDigitizingToolsRegistry->addDefaultTools();
+
mWidgetStateHelper = new QgsWidgetStateHelper();
mProcessingFavoriteAlgorithmManager = new QgsProcessingFavoriteAlgorithmManager();
mProcessingRecentAlgorithmLog = new QgsProcessingRecentAlgorithmLog();
diff --git a/src/gui/qgsgui.h b/src/gui/qgsgui.h
index 8fcf8afe71ee..d45056f4736f 100644
--- a/src/gui/qgsgui.h
+++ b/src/gui/qgsgui.h
@@ -32,6 +32,7 @@ class QgsSourceSelectProviderRegistry;
class QgsNative;
class QgsLayoutItemGuiRegistry;
class QgsAnnotationItemGuiRegistry;
+class QgsAdvancedDigitizingToolsRegistry;
class QgsWidgetStateHelper;
class QgsProcessingGuiRegistry;
class QgsProcessingFavoriteAlgorithmManager;
@@ -131,6 +132,13 @@ class GUI_EXPORT QgsGui : public QObject
*/
static QgsAnnotationItemGuiRegistry *annotationItemGuiRegistry() SIP_KEEPREFERENCE;
+ /**
+ * Returns the global advanced digitizing tools registry, used for registering advanced digitizing tools.
+ *
+ * \since QGIS 3.40
+ */
+ static QgsAdvancedDigitizingToolsRegistry *advancedDigitizingToolsRegistry() SIP_KEEPREFERENCE;
+
/**
* Returns the global processing gui registry, used for registering the GUI behavior of processing algorithms.
* \since QGIS 3.2
@@ -334,6 +342,7 @@ class GUI_EXPORT QgsGui : public QObject
QgsMapLayerActionRegistry *mMapLayerActionRegistry = nullptr;
QgsLayoutItemGuiRegistry *mLayoutItemGuiRegistry = nullptr;
QgsAnnotationItemGuiRegistry *mAnnotationItemGuiRegistry = nullptr;
+ QgsAdvancedDigitizingToolsRegistry *mAdvancedDigitizingToolsRegistry = nullptr;
QgsProcessingGuiRegistry *mProcessingGuiRegistry = nullptr;
QgsProcessingFavoriteAlgorithmManager *mProcessingFavoriteAlgorithmManager = nullptr;
QgsProcessingRecentAlgorithmLog *mProcessingRecentAlgorithmLog = nullptr;
diff --git a/src/gui/qgsmaptooladvanceddigitizing.cpp b/src/gui/qgsmaptooladvanceddigitizing.cpp
index ae2f19dce5e2..94f3d804a2e0 100644
--- a/src/gui/qgsmaptooladvanceddigitizing.cpp
+++ b/src/gui/qgsmaptooladvanceddigitizing.cpp
@@ -35,10 +35,12 @@ void QgsMapToolAdvancedDigitizing::canvasPressEvent( QgsMapMouseEvent *e )
{
if ( isAdvancedDigitizingAllowed() && mCadDockWidget->cadEnabled() )
{
- mCadDockWidget->applyConstraints( e ); // updates event's map point
-
- if ( mCadDockWidget->constructionMode() )
- return; // decided to eat the event and not pass it to the map tool (construction mode)
+ mCadDockWidget->applyConstraints( e ); // updates event's map point
+ mCadDockWidget->processCanvasPressEvent( e );
+ if ( !e->isAccepted() )
+ {
+ return; // The dock widget has taken the event
+ }
}
else if ( isAutoSnapEnabled() )
{
@@ -64,20 +66,12 @@ void QgsMapToolAdvancedDigitizing::canvasReleaseEvent( QgsMapMouseEvent *e )
}
else
{
- mCadDockWidget->applyConstraints( e ); // updates event's map point
-
- if ( mCadDockWidget->alignToSegment( e ) )
+ mCadDockWidget->applyConstraints( e ); // updates event's map point
+ mCadDockWidget->processCanvasReleaseEvent( e );
+ if ( !e->isAccepted() )
{
- // Parallel or perpendicular mode and snapped to segment: do not pass the event to map tool
- return;
+ return; // The dock widget has taken the event
}
-
- mCadDockWidget->addPoint( e->mapPoint() );
-
- mCadDockWidget->releaseLocks( false );
-
- if ( mCadDockWidget->constructionMode() )
- return; // decided to eat the event and not pass it to the map tool (construction mode)
}
}
else if ( isAutoSnapEnabled() )
@@ -98,12 +92,12 @@ void QgsMapToolAdvancedDigitizing::canvasMoveEvent( QgsMapMouseEvent *e )
{
if ( isAdvancedDigitizingAllowed() && mCadDockWidget->cadEnabled() )
{
- mCadDockWidget->applyConstraints( e ); // updates event's map point
-
- // perpendicular/parallel constraint
- // do a soft lock when snapping to a segment
- mCadDockWidget->alignToSegment( e, QgsAdvancedDigitizingDockWidget::CadConstraint::SoftLock );
- mCadDockWidget->updateCadPaintItem();
+ mCadDockWidget->applyConstraints( e ); // updates event's map point
+ mCadDockWidget->processCanvasMoveEvent( e );
+ if ( !e->isAccepted() )
+ {
+ return; // The dock widget has taken the event
+ }
}
else if ( isAutoSnapEnabled() )
{
diff --git a/src/ui/qgsadvanceddigitizingdockwidgetbase.ui b/src/ui/qgsadvanceddigitizingdockwidgetbase.ui
index ac8e5ab24976..68b57cbccaa4 100644
--- a/src/ui/qgsadvanceddigitizingdockwidgetbase.ui
+++ b/src/ui/qgsadvanceddigitizingdockwidgetbase.ui
@@ -46,12 +46,13 @@
+
+
+
-
-
-
@@ -605,6 +606,15 @@
+
+
+ true
+
+
+
+ :/images/themes/default/cadtools/circlesintersection.svg:/images/themes/default/cadtools/circlesintersection.svg
+
+
diff --git a/tests/src/gui/CMakeLists.txt b/tests/src/gui/CMakeLists.txt
index 94edb3c983fe..a1d151f974e0 100644
--- a/tests/src/gui/CMakeLists.txt
+++ b/tests/src/gui/CMakeLists.txt
@@ -25,6 +25,7 @@ endif()
set(TESTS
testqgsadvanceddigitizingdockwidget.cpp
+ testqgsadvanceddigitizingtoolsregistry.cpp
testqgsannotationitemguiregistry.cpp
testqgsmaptoolzoom.cpp
testqgsmaptooledit.cpp
diff --git a/tests/src/gui/testqgsadvanceddigitizingtoolsregistry.cpp b/tests/src/gui/testqgsadvanceddigitizingtoolsregistry.cpp
new file mode 100644
index 000000000000..b04222632049
--- /dev/null
+++ b/tests/src/gui/testqgsadvanceddigitizingtoolsregistry.cpp
@@ -0,0 +1,95 @@
+/***************************************************************************
+ testqgsadvanceddigitizingtoolsregistry.cpp
+ --------------------
+ Date : July 2024
+ Copyright : (C) 2024 Mathieu Pellerin
+ Email : mathieu 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 "qgstest.h"
+#include "qgsadvanceddigitizingtools.h"
+#include "qgsadvanceddigitizingtoolsregistry.h"
+#include
+
+class TestQgsAdvancedDigitizingToolsRegistry: public QObject
+{
+ Q_OBJECT
+ private slots:
+ void initTestCase(); // will be called before the first testfunction is executed.
+ void cleanupTestCase(); // will be called after the last testfunction was executed.
+ void init(); // will be called before each testfunction is executed.
+ void cleanup(); // will be called after every testfunction.
+ void guiRegistry();
+
+ private:
+
+};
+
+void TestQgsAdvancedDigitizingToolsRegistry::initTestCase()
+{
+}
+
+void TestQgsAdvancedDigitizingToolsRegistry::cleanupTestCase()
+{
+}
+
+void TestQgsAdvancedDigitizingToolsRegistry::init()
+{
+}
+
+void TestQgsAdvancedDigitizingToolsRegistry::cleanup()
+{
+}
+
+class DummyAdvancedDigitizingTool : public QgsAdvancedDigitizingTool
+{
+ Q_OBJECT
+ public:
+ DummyAdvancedDigitizingTool()
+ : QgsAdvancedDigitizingTool( nullptr, nullptr )
+ {}
+};
+
+void TestQgsAdvancedDigitizingToolsRegistry::guiRegistry()
+{
+ // test QgsAnnotationItemGuiRegistry
+ QgsAdvancedDigitizingToolsRegistry registry;
+
+ // empty registry
+ QVERIFY( !registry.toolMetadata( QString( "empty" ) ) );
+ QVERIFY( registry.toolMetadataNames().isEmpty() );
+
+ auto createTool = []( QgsMapCanvas *, QgsAdvancedDigitizingDockWidget * )->QgsAdvancedDigitizingTool *
+ {
+ return new DummyAdvancedDigitizingTool();
+ };
+
+ QgsAdvancedDigitizingToolMetadata *metadata = new QgsAdvancedDigitizingToolMetadata( QStringLiteral( "dummy" ), QStringLiteral( "My Dummy Tool" ), QIcon(), createTool );
+ QVERIFY( registry.addTool( metadata ) );
+ const QString name = registry.toolMetadataNames().value( 0 );
+ QCOMPARE( name, QStringLiteral( "dummy" ) );
+
+ // duplicate name not allowed
+ metadata = new QgsAdvancedDigitizingToolMetadata( QStringLiteral( "dummy" ), QStringLiteral( "My Dummy Tool" ), QIcon(), createTool );
+ QVERIFY( !registry.addTool( metadata ) );
+
+ QVERIFY( registry.toolMetadata( name ) );
+ QCOMPARE( registry.toolMetadata( name )->visibleName(), QStringLiteral( "My Dummy Tool" ) );
+
+ QgsAdvancedDigitizingTool *tool = registry.toolMetadata( name )->createTool( nullptr, nullptr );
+ QVERIFY( tool );
+
+ registry.removeTool( name );
+ QVERIFY( registry.toolMetadataNames().isEmpty() );
+}
+
+
+QGSTEST_MAIN( TestQgsAdvancedDigitizingToolsRegistry )
+#include "testqgsadvanceddigitizingtoolsregistry.moc"