Skip to content

Commit

Permalink
Add iface method and new class for delegating all responsibility
Browse files Browse the repository at this point in the history
and logic for activating a custom map tool and ensuring it can
only be enabled in the right circumstances to QGIS app

If a plugin has to do this, it's a nightmare of code and hacks (partly
because of the number of changing circumstances it needs to respond
to, and partly because a lot of the useful functions available
for handling this behavior is locked away in private methods
in qgisapp.cpp)

So instead make an abstract base class for map tool handlers and
an iface method for register/unregistering them.

From the dox:

An abstract base class for map tool handlers which automatically handle all the necessary
logic for toggling the map tool and enabling/disabling the associated action
when the QGIS application is in a state permissible for the tool.

Creating these handlers avoids a lot of complex setup code and manual connections
which are otherwise necessary to ensure that a map tool is correctly activated and
deactivated when the state of the QGIS application changes (e.g. when the active
layer is changed, when edit modes are toggled, when other map tools are switched
to, etc).

- ### Example

\code{.py}
  class MyMapTool(QgsMapTool):
     ...

  class MyMapToolHandler(QgsAbstractMapToolHandler):

     def __init__(self, tool, action):
         super().__init__(tool, action)

     def isCompatibleWithLayer(self, layer, context):
         # this tool can only be activated when an editable vector layer is selected
         return isinstance(layer, QgsVectorLayer) and layer.isEditable()

  my_tool = MyMapTool()
  my_action = QAction('My Map Tool')

  my_handler = MyMapToolHandler(my_tool, my_action)
  iface.registerMapToolHandler(my_handler)
\endcode
  • Loading branch information
nyalldawson committed Oct 23, 2020
1 parent 19842ea commit e0321be
Show file tree
Hide file tree
Showing 11 changed files with 451 additions and 0 deletions.
27 changes: 27 additions & 0 deletions python/gui/auto_generated/qgisinterface.sip.in
Expand Up @@ -1267,6 +1267,33 @@ Unregister a previously registered application exit ``blocker``.

.. seealso:: :py:func:`registerApplicationExitBlocker`

.. versionadded:: 3.16
%End

virtual void registerMapToolHandler( QgsAbstractMapToolHandler *handler ) = 0;
%Docstring
Register a new application map tool ``handler``, which can be used to automatically setup all connections
and logic required to switch to a custom map tool whenever the state of the QGIS application
permits.

.. note::

Ownership of ``handler`` is not transferred, and the handler must
be unregistered when plugin is unloaded.

.. seealso:: :py:class:`QgsAbstractMapToolHandler`

.. seealso:: :py:func:`unregisterMapToolHandler`

.. versionadded:: 3.16
%End

virtual void unregisterMapToolHandler( QgsAbstractMapToolHandler *handler ) = 0;
%Docstring
Unregister a previously registered map tool ``handler``.

.. seealso:: :py:func:`registerMapToolHandler`

.. versionadded:: 3.16
%End

Expand Down
115 changes: 115 additions & 0 deletions python/gui/auto_generated/qgsabstractmaptoolhandler.sip.in
@@ -0,0 +1,115 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/gui/qgsabstractmaptoolhandler.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/




class QgsAbstractMapToolHandler
{
%Docstring
An abstract base class for map tool handlers which automatically handle all the necessary
logic for toggling the map tool and enabling/disabling the associated action
when the QGIS application is in a state permissible for the tool.

Creating these handlers avoids a lot of complex setup code and manual connections
which are otherwise necessary to ensure that a map tool is correctly activated and
deactivated when the state of the QGIS application changes (e.g. when the active
layer is changed, when edit modes are toggled, when other map tools are switched
to, etc).

- ### Example

.. code-block:: python

class MyMapTool(QgsMapTool):
...

class MyMapToolHandler(QgsAbstractMapToolHandler):

def __init__(self, tool, action):
super().__init__(tool, action)

def isCompatibleWithLayer(self, layer, context):
# this tool can only be activated when an editable vector layer is selected
return isinstance(layer, QgsVectorLayer) and layer.isEditable()

my_tool = MyMapTool()
my_action = QAction('My Map Tool')

my_handler = MyMapToolHandler(my_tool, my_action)
iface.registerMapToolHandler(my_handler)

.. versionadded:: 3.16
%End

%TypeHeaderCode
#include "qgsabstractmaptoolhandler.h"
%End
public:

struct Context
{
bool dummy;
};

QgsAbstractMapToolHandler( QgsMapTool *tool, QAction *action );
%Docstring
Constructor for a map tool handler for the specified ``tool``.

The ``action`` argument must be set to the action associated with switching
to the tool.

The ownership of neither ``tool`` nor ``action`` is transferred, and the caller
is responsible for ensuring that these objects exist for the lifetime of the
handler.

.. warning::

The handler will be responsible for creating the appropriate
connections between the ``action`` and the ``tool``. These should NOT be
manually connected elsewhere!
%End

virtual ~QgsAbstractMapToolHandler();

QgsMapTool *mapTool();
%Docstring
Returns the tool associated with this handler.
%End

QAction *action();
%Docstring
Returns the action associated with toggling the tool.
%End

virtual bool isCompatibleWithLayer( QgsMapLayer *layer, const QgsAbstractMapToolHandler::Context &context ) = 0;
%Docstring
Returns ``True`` if the associated map tool is compatible with the specified ``layer``.

Additional information is available through the ``context`` argument.
%End

virtual void setLayerForTool( QgsMapLayer *layer );
%Docstring
Sets the ``layer`` to use for the tool.

Called whenever a new layer should be associated with the tool, e.g. as a result of the
user selecting a different active layer.

The default implementation does nothing.
%End

};

/************************************************************************
* This file has been generated automatically from *
* *
* src/gui/qgsabstractmaptoolhandler.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
1 change: 1 addition & 0 deletions python/gui/gui_auto.sip
Expand Up @@ -2,6 +2,7 @@
%Include auto_generated/qgisinterface.sip
%Include auto_generated/qgs3dsymbolwidget.sip
%Include auto_generated/qgsabstractdatasourcewidget.sip
%Include auto_generated/qgsabstractmaptoolhandler.sip
%Include auto_generated/qgsactionmenu.sip
%Include auto_generated/qgsadvanceddigitizingdockwidget.sip
%Include auto_generated/qgsadvanceddigitizingfloater.sip
Expand Down
78 changes: 78 additions & 0 deletions src/app/qgisapp.cpp
Expand Up @@ -163,6 +163,7 @@ Q_GUI_EXPORT extern int qt_defaultDpiX();
#include "qgis.h"
#include "qgisplugin.h"
#include "qgsabout.h"
#include "qgsabstractmaptoolhandler.h"
#include "qgsalignrasterdialog.h"
#include "qgsappauthrequesthandler.h"
#include "qgsappbrowserproviders.h"
Expand Down Expand Up @@ -12703,6 +12704,65 @@ void QgisApp::unregisterApplicationExitBlocker( QgsApplicationExitBlockerInterfa
mApplicationExitBlockers.removeAll( blocker );
}

void QgisApp::registerMapToolHandler( QgsAbstractMapToolHandler *handler )
{
if ( !handler->action() || !handler->mapTool() )
{
QgsMessageLog::logMessage( tr( "Map tool handler is not properly constructed" ) );
return;
}

mMapToolHandlers << handler;

// do setup work
handler->action()->setCheckable( true );
handler->mapTool()->setAction( handler->action() );

connect( handler->action(), &QAction::triggered, this, &QgisApp::switchToMapToolViaHandler );
mMapToolGroup->addAction( handler->action() );
QgsAbstractMapToolHandler::Context context;
handler->action()->setEnabled( handler->isCompatibleWithLayer( activeLayer(), context ) );
}

void QgisApp::switchToMapToolViaHandler()
{
QAction *sourceAction = qobject_cast< QAction * >( sender() );
if ( !sourceAction )
return;

QgsAbstractMapToolHandler *handler = nullptr;
for ( QgsAbstractMapToolHandler *h : qgis::as_const( mMapToolHandlers ) )
{
if ( h->action() == sourceAction )
{
handler = h;
break;
}
}

if ( !handler )
return;

if ( mMapCanvas->mapTool() == handler->mapTool() )
return; // nothing to do

handler->setLayerForTool( activeLayer() );
mMapCanvas->setMapTool( handler->mapTool() );
}

void QgisApp::unregisterMapToolHandler( QgsAbstractMapToolHandler *handler )
{
mMapToolHandlers.removeAll( handler );

if ( !handler->action() || !handler->mapTool() )
{
return;
}

mMapToolGroup->removeAction( handler->action() );
disconnect( handler->action(), &QAction::triggered, this, &QgisApp::switchToMapToolViaHandler );
}

QgsMapLayer *QgisApp::activeLayer()
{
return mLayerTreeView ? mLayerTreeView->currentLayer() : nullptr;
Expand Down Expand Up @@ -14333,6 +14393,24 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer *layer )

updateLayerModifiedActions();

QgsAbstractMapToolHandler::Context context;
for ( QgsAbstractMapToolHandler *handler : qgis::as_const( mMapToolHandlers ) )
{
handler->action()->setEnabled( handler->isCompatibleWithLayer( layer, context ) );
if ( handler->mapTool() == mMapCanvas->mapTool() )
{
if ( !handler->action()->isEnabled() )
{
mMapCanvas->unsetMapTool( handler->mapTool() );
mActionPan->trigger();
}
else
{
handler->setLayerForTool( layer );
}
}
}

if ( !layer )
{
menuSelect->setEnabled( false );
Expand Down
23 changes: 23 additions & 0 deletions src/app/qgisapp.h
Expand Up @@ -112,6 +112,7 @@ class QgsHandleBadLayersHandler;
class QgsNetworkAccessManager;
class QgsGpsConnection;
class QgsApplicationExitBlockerInterface;
class QgsAbstractMapToolHandler;

class QDomDocument;
class QNetworkReply;
Expand Down Expand Up @@ -764,6 +765,25 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
*/
void unregisterApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker );

/**
* Register a new application map tool \a handler, which can be used to automatically setup all connections
* and logic required to switch to a custom map tool whenever the state of the QGIS application
* permits.
*
* \note Ownership of \a handler is not transferred, and the handler must
* be unregistered when plugin is unloaded.
*
* \see QgsAbstractMapToolHandler
* \see unregisterMapToolHandler()
*/
void registerMapToolHandler( QgsAbstractMapToolHandler *handler );

/**
* Unregister a previously registered map tool \a handler.
* \see registerMapToolHandler()
*/
void unregisterMapToolHandler( QgsAbstractMapToolHandler *handler );

//! Register a new custom drop handler.
void registerCustomDropHandler( QgsCustomDropHandler *handler );

Expand Down Expand Up @@ -2167,6 +2187,8 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
//! Configure layer tree view according to the user options from QgsSettings
void setupLayerTreeViewFromSettings();

void switchToMapToolViaHandler();

void readSettings();
void writeSettings();
void createActions();
Expand Down Expand Up @@ -2595,6 +2617,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
QList<QgsDevToolWidgetFactory * > mDevToolFactories;

QList<QgsApplicationExitBlockerInterface * > mApplicationExitBlockers;
QList<QgsAbstractMapToolHandler * > mMapToolHandlers;

QVector<QPointer<QgsCustomDropHandler>> mCustomDropHandlers;
QVector<QPointer<QgsCustomProjectOpenHandler>> mCustomProjectOpenHandlers;
Expand Down
10 changes: 10 additions & 0 deletions src/app/qgisappinterface.cpp
Expand Up @@ -581,6 +581,16 @@ void QgisAppInterface::unregisterApplicationExitBlocker( QgsApplicationExitBlock
qgis->unregisterApplicationExitBlocker( blocker );
}

void QgisAppInterface::registerMapToolHandler( QgsAbstractMapToolHandler *handler )
{
qgis->registerMapToolHandler( handler );
}

void QgisAppInterface::unregisterMapToolHandler( QgsAbstractMapToolHandler *handler )
{
qgis->unregisterMapToolHandler( handler );
}

void QgisAppInterface::registerCustomDropHandler( QgsCustomDropHandler *handler )
{
qgis->registerCustomDropHandler( handler );
Expand Down
2 changes: 2 additions & 0 deletions src/app/qgisappinterface.h
Expand Up @@ -152,6 +152,8 @@ class APP_EXPORT QgisAppInterface : public QgisInterface
void unregisterDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) override;
void registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) override;
void unregisterApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) override;
void registerMapToolHandler( QgsAbstractMapToolHandler *handler ) override;
void unregisterMapToolHandler( QgsAbstractMapToolHandler *handler ) override;
void registerCustomDropHandler( QgsCustomDropHandler *handler ) override;
void unregisterCustomDropHandler( QgsCustomDropHandler *handler ) override;
void registerCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ) override;
Expand Down
2 changes: 2 additions & 0 deletions src/gui/CMakeLists.txt
Expand Up @@ -359,6 +359,7 @@ SET(QGIS_GUI_SRCS

qgisinterface.cpp
qgs3dsymbolwidget.cpp
qgsabstractmaptoolhandler.cpp
qgsactionmenu.cpp
qgsaddattrdialog.cpp
qgsaddtaborgroup.cpp
Expand Down Expand Up @@ -589,6 +590,7 @@ SET(QGIS_GUI_HDRS

qgs3dsymbolwidget.h
qgsabstractdatasourcewidget.h
qgsabstractmaptoolhandler.h
qgsactionmenu.h
qgsaddattrdialog.h
qgsaddtaborgroup.h
Expand Down
22 changes: 22 additions & 0 deletions src/gui/qgisinterface.h
Expand Up @@ -67,6 +67,7 @@ class QgsBrowserGuiModel;
class QgsDevToolWidgetFactory;
class QgsGpsConnection;
class QgsApplicationExitBlockerInterface;
class QgsAbstractMapToolHandler;


/**
Expand Down Expand Up @@ -1056,6 +1057,27 @@ class GUI_EXPORT QgisInterface : public QObject
*/
virtual void unregisterApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) = 0;

/**
* Register a new application map tool \a handler, which can be used to automatically setup all connections
* and logic required to switch to a custom map tool whenever the state of the QGIS application
* permits.
*
* \note Ownership of \a handler is not transferred, and the handler must
* be unregistered when plugin is unloaded.
*
* \see QgsAbstractMapToolHandler
* \see unregisterMapToolHandler()
* \since QGIS 3.16
*/
virtual void registerMapToolHandler( QgsAbstractMapToolHandler *handler ) = 0;

/**
* Unregister a previously registered map tool \a handler.
* \see registerMapToolHandler()
* \since QGIS 3.16
*/
virtual void unregisterMapToolHandler( QgsAbstractMapToolHandler *handler ) = 0;

/**
* Register a new custom drop \a handler.
* \note Ownership of \a handler is not transferred, and the handler must
Expand Down

0 comments on commit e0321be

Please sign in to comment.