Follow up map layer action changes

- Switch to flags instead of boolean argument
- Move logic for layer validity to canRunUsingLayer
- Add unit test

Also remove settings flag to hide duplicate features action
This commit is contained in:
Nyall Dawson 2018-02-24 15:52:08 +10:00
parent 1bada065ed
commit f35745fc70
11 changed files with 214 additions and 71 deletions

View File

@ -30,7 +30,15 @@ An action which can run on map layers
typedef QFlags<QgsMapLayerAction::Target> Targets;
QgsMapLayerAction( const QString &name, QObject *parent /TransferThis/, Targets targets = AllActions, const QIcon &icon = QIcon() );
enum Flag
{
EnabledOnlyWhenEditable,
};
typedef QFlags<QgsMapLayerAction::Flag> Flags;
QgsMapLayerAction( const QString &name, QObject *parent /TransferThis/, Targets targets = AllActions, const QIcon &icon = QIcon(), QgsMapLayerAction::Flags flags = 0 );
%Docstring
Creates a map layer action which can run on any layer
@ -39,18 +47,25 @@ Creates a map layer action which can run on any layer
using AllActions as a target probably does not make a lot of sense. This default action was settled for API compatibility reasons.
%End
QgsMapLayerAction( const QString &name, QObject *parent /TransferThis/, QgsMapLayer *layer, Targets targets = AllActions, const QIcon &icon = QIcon() );
QgsMapLayerAction( const QString &name, QObject *parent /TransferThis/, QgsMapLayer *layer, Targets targets = AllActions, const QIcon &icon = QIcon(), QgsMapLayerAction::Flags flags = 0 );
%Docstring
Creates a map layer action which can run only on a specific layer
%End
QgsMapLayerAction( const QString &name, QObject *parent /TransferThis/, QgsMapLayer::LayerType layerType, Targets targets = AllActions, const QIcon &icon = QIcon() );
QgsMapLayerAction( const QString &name, QObject *parent /TransferThis/, QgsMapLayer::LayerType layerType, Targets targets = AllActions, const QIcon &icon = QIcon(), QgsMapLayerAction::Flags flags = 0 );
%Docstring
Creates a map layer action which can run on a specific type of layer
%End
~QgsMapLayerAction();
QgsMapLayerAction::Flags flags() const;
%Docstring
Layer behavior flags.
.. versionadded:: 3.0
%End
bool canRunUsingLayer( QgsMapLayer *layer ) const;
%Docstring
True if action can run using the specified layer
@ -78,6 +93,13 @@ Define the targets of the action
const Targets &targets() const;
%Docstring
Return availibity of action
%End
bool isEnabledOnlyWhenEditable() const;
%Docstring
Returns true if the action is only enabled for layers in editable mode.
.. versionadded:: 3.0
%End
signals:
@ -158,6 +180,9 @@ Triggered when an action is added or removed from the registry
};
QFlags<QgsMapLayerAction::Flag> operator|(QgsMapLayerAction::Flag f1, QFlags<QgsMapLayerAction::Flag> f2);
/************************************************************************
* This file has been generated automatically from *
* *

View File

@ -34,7 +34,3 @@ connections-xyz\OpenStreetMap\zmin=0
# for now this is online version of the User Guide for latest (LTR) release
helpSearchPath=https://docs.qgis.org/$qgis_short_version/$qgis_locale/docs/user_manual/
[app]
# If true, the experimental "duplicate feature" actions will be shown in the QGIS UI
tools\showDuplicateFeatureActions=false

View File

@ -6488,9 +6488,6 @@ void QgisApp::refreshFeatureActions()
for ( int i = 0; i < registeredActions.size(); i++ )
{
if ( !vlayer->isEditable() && registeredActions.at( i )->isEnabledOnlyWhenEditable() )
continue;
mFeatureActionMenu->addAction( registeredActions.at( i ) );
if ( registeredActions.at( i ) == QgsGui::mapLayerActionRegistry()->defaultActionForLayer( vlayer ) )
{
@ -7498,31 +7495,27 @@ void QgisApp::setupLayoutManagerConnections()
void QgisApp::setupDuplicateFeaturesAction()
{
QgsSettings settings;
if ( settings.value( QStringLiteral( "tools/showDuplicateFeatureActions" ), false, QgsSettings::App ).toBool() )
mDuplicateFeatureAction.reset( new QgsMapLayerAction( tr( "Duplicate feature" ),
nullptr, QgsMapLayerAction::SingleFeature,
QgsApplication::getThemeIcon( QStringLiteral( "/mActionDuplicateFeature.svg" ) ), QgsMapLayerAction::EnabledOnlyWhenEditable ) );
QgsGui::mapLayerActionRegistry()->addMapLayerAction( mDuplicateFeatureAction.get() );
connect( mDuplicateFeatureAction.get(), &QgsMapLayerAction::triggeredForFeature, this, [this]( QgsMapLayer * layer, const QgsFeature & feat )
{
mDuplicateFeatureAction.reset( new QgsMapLayerAction( tr( "Duplicate feature" ),
nullptr, QgsMapLayerAction::SingleFeature,
QgsApplication::getThemeIcon( QStringLiteral( "/mActionDuplicateFeature.svg" ) ), true ) );
QgsGui::mapLayerActionRegistry()->addMapLayerAction( mDuplicateFeatureAction.get() );
connect( mDuplicateFeatureAction.get(), &QgsMapLayerAction::triggeredForFeature, this, [this]( QgsMapLayer * layer, const QgsFeature & feat )
{
duplicateFeatures( layer, feat );
}
);
mDuplicateFeatureDigitizeAction.reset( new QgsMapLayerAction( tr( "Duplicate feature and digitize" ),
nullptr, QgsMapLayerAction::SingleFeature,
QgsApplication::getThemeIcon( QStringLiteral( "/mActionDuplicateFeatureDigitized.svg" ) ), true ) );
QgsGui::mapLayerActionRegistry()->addMapLayerAction( mDuplicateFeatureDigitizeAction.get() );
connect( mDuplicateFeatureDigitizeAction.get(), &QgsMapLayerAction::triggeredForFeature, this, [this]( QgsMapLayer * layer, const QgsFeature & feat )
{
duplicateFeatureDigitized( layer, feat );
}
);
duplicateFeatures( layer, feat );
}
);
mDuplicateFeatureDigitizeAction.reset( new QgsMapLayerAction( tr( "Duplicate feature and digitize" ),
nullptr, QgsMapLayerAction::SingleFeature,
QgsApplication::getThemeIcon( QStringLiteral( "/mActionDuplicateFeatureDigitized.svg" ) ), QgsMapLayerAction::EnabledOnlyWhenEditable ) );
QgsGui::mapLayerActionRegistry()->addMapLayerAction( mDuplicateFeatureDigitizeAction.get() );
connect( mDuplicateFeatureDigitizeAction.get(), &QgsMapLayerAction::triggeredForFeature, this, [this]( QgsMapLayer * layer, const QgsFeature & feat )
{
duplicateFeatureDigitized( layer, feat );
}
);
}
void QgisApp::setupAtlasMapLayerAction( QgsPrintLayout *layout, bool enableAction )

View File

@ -201,9 +201,6 @@ QWidget *QgsAttributeTableView::createActionWidget( QgsFeatureId fid )
QgsGui::mapLayerActionRegistry()->mapLayerActions( mFilterModel->layer(),
QgsMapLayerAction::SingleFeature ) )
{
if ( !mFilterModel->layer()->isEditable() && mapLayerAction->isEnabledOnlyWhenEditable() )
continue;
QAction *action = new QAction( mapLayerAction->icon(), mapLayerAction->text(), container );
action->setData( "map_layer_action" );
action->setToolTip( mapLayerAction->text() );

View File

@ -592,9 +592,6 @@ void QgsDualView::viewWillShowContextMenu( QMenu *menu, const QModelIndex &atInd
Q_FOREACH ( QgsMapLayerAction *action, registeredActions )
{
if ( !vl->isEditable() && action->isEnabledOnlyWhenEditable() )
continue;
QgsAttributeTableMapLayerAction *a = new QgsAttributeTableMapLayerAction( action->text(), this, action, sourceIndex );
#if QT_VERSION < QT_VERSION_CHECK(5, 6, 0)
menu->addAction( action->text(), a, SLOT( execut() ) );

View File

@ -154,9 +154,6 @@ void QgsActionMenu::reloadActions()
{
QgsMapLayerAction *qaction = mapLayerActions.at( i );
if ( !mLayer->isEditable() && qaction->isEnabledOnlyWhenEditable() )
continue;
if ( qaction->isEnabledOnlyWhenEditable() && ( mMode == QgsAttributeForm::AddFeatureMode || mMode == QgsAttributeForm::IdentifyMode ) )
continue;

View File

@ -346,6 +346,7 @@ void QgsIdentifyMenu::addVectorLayer( QgsVectorLayer *layer, const QList<QgsMapT
if ( mShowFeatureActions )
{
featureActionMenu = new QgsActionMenu( layer, result.mFeature, QStringLiteral( "Feature" ), layerMenu );
featureActionMenu->setMode( QgsAttributeForm::IdentifyMode );
featureActionMenu->setExpressionContextScope( mExpressionContextScope );
}

View File

@ -15,35 +15,36 @@
#include "qgsmaplayeractionregistry.h"
#include "qgsgui.h"
#include "qgsvectorlayer.h"
QgsMapLayerAction::QgsMapLayerAction( const QString &name, QObject *parent, Targets targets, const QIcon &icon, const bool enabledOnlyWhenEditable )
QgsMapLayerAction::QgsMapLayerAction( const QString &name, QObject *parent, Targets targets, const QIcon &icon, QgsMapLayerAction::Flags flags )
: QAction( icon, name, parent )
, mSingleLayer( false )
, mSpecificLayerType( false )
, mLayerType( QgsMapLayer::VectorLayer )
, mTargets( targets )
, mEnabledOnlyWhenEditable( enabledOnlyWhenEditable )
, mFlags( flags )
{
}
QgsMapLayerAction::QgsMapLayerAction( const QString &name, QObject *parent, QgsMapLayer *layer, Targets targets, const QIcon &icon, const bool enabledOnlyWhenEditable )
QgsMapLayerAction::QgsMapLayerAction( const QString &name, QObject *parent, QgsMapLayer *layer, Targets targets, const QIcon &icon, QgsMapLayerAction::Flags flags )
: QAction( icon, name, parent )
, mSingleLayer( true )
, mActionLayer( layer )
, mSpecificLayerType( false )
, mLayerType( QgsMapLayer::VectorLayer )
, mTargets( targets )
, mEnabledOnlyWhenEditable( enabledOnlyWhenEditable )
, mFlags( flags )
{
}
QgsMapLayerAction::QgsMapLayerAction( const QString &name, QObject *parent, QgsMapLayer::LayerType layerType, Targets targets, const QIcon &icon, const bool enabledOnlyWhenEditable )
QgsMapLayerAction::QgsMapLayerAction( const QString &name, QObject *parent, QgsMapLayer::LayerType layerType, Targets targets, const QIcon &icon, QgsMapLayerAction::Flags flags )
: QAction( icon, name, parent )
, mSingleLayer( false )
, mSpecificLayerType( true )
, mLayerType( layerType )
, mTargets( targets )
, mEnabledOnlyWhenEditable( enabledOnlyWhenEditable )
, mFlags( flags )
{
}
@ -53,8 +54,24 @@ QgsMapLayerAction::~QgsMapLayerAction()
QgsGui::mapLayerActionRegistry()->removeMapLayerAction( this );
}
QgsMapLayerAction::Flags QgsMapLayerAction::flags() const
{
return mFlags;
}
bool QgsMapLayerAction::canRunUsingLayer( QgsMapLayer *layer ) const
{
if ( mFlags & EnabledOnlyWhenEditable )
{
// action is only enabled for editable layers
if ( !layer )
return false;
if ( layer->type() != QgsMapLayer::VectorLayer )
return false;
if ( !qobject_cast<QgsVectorLayer *>( layer )->isEditable() )
return false;
}
//check layer details
if ( !mSingleLayer && !mSpecificLayerType )
{
@ -67,7 +84,7 @@ bool QgsMapLayerAction::canRunUsingLayer( QgsMapLayer *layer ) const
//action is a single layer type and layer matches
return true;
}
else if ( mSpecificLayerType && layer->type() == mLayerType )
else if ( mSpecificLayerType && layer && layer->type() == mLayerType )
{
//action is for a layer type and layer type matches
return true;
@ -91,6 +108,11 @@ void QgsMapLayerAction::triggerForLayer( QgsMapLayer *layer )
emit triggeredForLayer( layer );
}
bool QgsMapLayerAction::isEnabledOnlyWhenEditable() const
{
return mFlags & EnabledOnlyWhenEditable;
}
//
// Main class begins now...
//

View File

@ -46,29 +46,42 @@ class GUI_EXPORT QgsMapLayerAction : public QAction
Q_DECLARE_FLAGS( Targets, Target )
Q_FLAG( Targets )
/**
* Flags which control action behavior
* /since QGIS 3.0
*/
enum Flag
{
EnabledOnlyWhenEditable = 1 << 1, //!< Action should be shown only for editable layers
};
/**
* Action behavior flags.
* \since QGIS 3.0
*/
Q_DECLARE_FLAGS( Flags, Flag )
Q_FLAG( Flags )
/**
* Creates a map layer action which can run on any layer
* \note using AllActions as a target probably does not make a lot of sense. This default action was settled for API compatibility reasons.
*/
#ifndef SIP_RUN
QgsMapLayerAction( const QString &name, QObject *parent SIP_TRANSFERTHIS, Targets targets = AllActions, const QIcon &icon = QIcon(), const bool enabledOnlyWhenEditable = false );
#else
QgsMapLayerAction( const QString &name, QObject *parent SIP_TRANSFERTHIS, Targets targets = AllActions, const QIcon &icon = QIcon() );
#endif
QgsMapLayerAction( const QString &name, QObject *parent SIP_TRANSFERTHIS, Targets targets = AllActions, const QIcon &icon = QIcon(), QgsMapLayerAction::Flags flags = nullptr );
//! Creates a map layer action which can run only on a specific layer
#ifndef SIP_RUN
QgsMapLayerAction( const QString &name, QObject *parent SIP_TRANSFERTHIS, QgsMapLayer *layer, Targets targets = AllActions, const QIcon &icon = QIcon(), const bool enabledOnlyWhenEditable = false );
#else
QgsMapLayerAction( const QString &name, QObject *parent SIP_TRANSFERTHIS, QgsMapLayer *layer, Targets targets = AllActions, const QIcon &icon = QIcon() );
#endif
QgsMapLayerAction( const QString &name, QObject *parent SIP_TRANSFERTHIS, QgsMapLayer *layer, Targets targets = AllActions, const QIcon &icon = QIcon(), QgsMapLayerAction::Flags flags = nullptr );
//! Creates a map layer action which can run on a specific type of layer
#ifndef SIP_RUN
QgsMapLayerAction( const QString &name, QObject *parent SIP_TRANSFERTHIS, QgsMapLayer::LayerType layerType, Targets targets = AllActions, const QIcon &icon = QIcon(), const bool enabledOnlyWhenEditable = false );
#else
QgsMapLayerAction( const QString &name, QObject *parent SIP_TRANSFERTHIS, QgsMapLayer::LayerType layerType, Targets targets = AllActions, const QIcon &icon = QIcon() );
#endif
QgsMapLayerAction( const QString &name, QObject *parent SIP_TRANSFERTHIS, QgsMapLayer::LayerType layerType, Targets targets = AllActions, const QIcon &icon = QIcon(), QgsMapLayerAction::Flags flags = nullptr );
~QgsMapLayerAction() override;
/**
* Layer behavior flags.
* \since QGIS 3.0
*/
QgsMapLayerAction::Flags flags() const;
//! True if action can run using the specified layer
bool canRunUsingLayer( QgsMapLayer *layer ) const;
@ -86,8 +99,11 @@ class GUI_EXPORT QgsMapLayerAction : public QAction
//! Return availibity of action
const Targets &targets() const {return mTargets;}
//! Return whether only enabled in editable mode
bool isEnabledOnlyWhenEditable() const { return mEnabledOnlyWhenEditable; }
/**
* Returns true if the action is only enabled for layers in editable mode.
* \since QGIS 3.0
*/
bool isEnabledOnlyWhenEditable() const;
signals:
//! Triggered when action has been run for a specific list of features
@ -102,19 +118,19 @@ class GUI_EXPORT QgsMapLayerAction : public QAction
private:
// true if action is only valid for a single layer
bool mSingleLayer;
bool mSingleLayer = false;
// layer if action is only valid for a single layer
QgsMapLayer *mActionLayer = nullptr;
// true if action is only valid for a specific layer type
bool mSpecificLayerType;
bool mSpecificLayerType = false;
// layer type if action is only valid for a specific layer type
QgsMapLayer::LayerType mLayerType;
QgsMapLayer::LayerType mLayerType = QgsMapLayer::VectorLayer;
// determine if the action can be run on layer and/or single feature and/or multiple features
Targets mTargets;
Targets mTargets = nullptr;
bool mEnabledOnlyWhenEditable;
QgsMapLayerAction::Flags mFlags = nullptr;
};
Q_DECLARE_OPERATORS_FOR_FLAGS( QgsMapLayerAction::Targets )
@ -168,4 +184,6 @@ class GUI_EXPORT QgsMapLayerActionRegistry : public QObject
};
Q_DECLARE_OPERATORS_FOR_FLAGS( QgsMapLayerAction::Flags )
#endif // QGSMAPLAYERACTIONREGISTRY_H

View File

@ -101,6 +101,7 @@ ADD_PYTHON_TEST(PyQgsLocator test_qgslocator.py)
ADD_PYTHON_TEST(PyQgsMapCanvas test_qgsmapcanvas.py)
ADD_PYTHON_TEST(PyQgsMapCanvasAnnotationItem test_qgsmapcanvasannotationitem.py)
ADD_PYTHON_TEST(PyQgsMapLayer test_qgsmaplayer.py)
ADD_PYTHON_TEST(PyQgsMapLayerAction test_qgsmaplayeraction.py)
ADD_PYTHON_TEST(PyQgsMapLayerModel test_qgsmaplayermodel.py)
ADD_PYTHON_TEST(PyQgsMapLayerStore test_qgsmaplayerstore.py)
ADD_PYTHON_TEST(PyQgsMapRenderer test_qgsmaprenderer.py)

View File

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsMapLayerAction.
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
__author__ = 'Nyall Dawson'
__date__ = '24/02/2018'
__copyright__ = 'Copyright 2018, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
import qgis # NOQA switch sip api
from qgis.core import (QgsVectorLayer,
QgsRasterLayer,
QgsMapLayer)
from qgis.gui import (QgsMapLayerActionRegistry,
QgsMapLayerAction)
from qgis.testing import start_app, unittest
from utilities import unitTestDataPath
import os
start_app()
class TestQgsMapLayerAction(unittest.TestCase):
def __init__(self, methodName):
"""Run once on class initialization."""
unittest.TestCase.__init__(self, methodName)
self.vector_layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer&field=flddate:datetime",
"test_layer", "memory")
assert self.vector_layer.isValid()
self.vector_layer2 = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer&field=flddate:datetime",
"test_layer", "memory")
assert self.vector_layer2.isValid()
raster_path = os.path.join(unitTestDataPath(), 'landsat.tif')
self.raster_layer = QgsRasterLayer(raster_path, 'raster')
assert self.raster_layer.isValid()
def testCanRunUsingLayer(self):
"""
Test that actions correctly indicate when they can run for a layer
"""
action_all_layers = QgsMapLayerAction('action1', None)
self.assertTrue(action_all_layers.canRunUsingLayer(None))
self.assertTrue(action_all_layers.canRunUsingLayer(self.vector_layer))
self.assertTrue(action_all_layers.canRunUsingLayer(self.raster_layer))
action_vector_layers_only = QgsMapLayerAction('action2', None, QgsMapLayer.VectorLayer)
self.assertFalse(action_vector_layers_only.canRunUsingLayer(None))
self.assertTrue(action_vector_layers_only.canRunUsingLayer(self.vector_layer))
self.assertFalse(action_vector_layers_only.canRunUsingLayer(self.raster_layer))
action_raster_layers_only = QgsMapLayerAction('action3', None, QgsMapLayer.RasterLayer)
self.assertFalse(action_raster_layers_only.canRunUsingLayer(None))
self.assertFalse(action_raster_layers_only.canRunUsingLayer(self.vector_layer))
self.assertTrue(action_raster_layers_only.canRunUsingLayer(self.raster_layer))
action_specific_layer_only = QgsMapLayerAction('action4', None, self.vector_layer)
self.assertFalse(action_specific_layer_only.canRunUsingLayer(None))
self.assertTrue(action_specific_layer_only.canRunUsingLayer(self.vector_layer))
self.assertFalse(action_specific_layer_only.canRunUsingLayer(self.vector_layer2))
self.assertFalse(action_specific_layer_only.canRunUsingLayer(self.raster_layer))
action_specific_raster_layer_only = QgsMapLayerAction('action4', None, self.raster_layer)
self.assertFalse(action_specific_raster_layer_only.canRunUsingLayer(None))
self.assertFalse(action_specific_raster_layer_only.canRunUsingLayer(self.vector_layer))
self.assertFalse(action_specific_raster_layer_only.canRunUsingLayer(self.vector_layer2))
self.assertTrue(action_specific_raster_layer_only.canRunUsingLayer(self.raster_layer))
action_editable_layer_only = QgsMapLayerAction('action1', None, flags=QgsMapLayerAction.EnabledOnlyWhenEditable)
self.assertFalse(action_editable_layer_only.canRunUsingLayer(None))
self.assertFalse(action_editable_layer_only.canRunUsingLayer(self.vector_layer))
self.assertFalse(action_editable_layer_only.canRunUsingLayer(self.vector_layer2))
self.assertFalse(action_editable_layer_only.canRunUsingLayer(self.raster_layer))
self.vector_layer.startEditing()
self.assertFalse(action_editable_layer_only.canRunUsingLayer(None))
self.assertTrue(action_editable_layer_only.canRunUsingLayer(self.vector_layer))
self.assertFalse(action_editable_layer_only.canRunUsingLayer(self.vector_layer2))
self.assertFalse(action_editable_layer_only.canRunUsingLayer(self.raster_layer))
self.vector_layer.commitChanges()
self.assertFalse(action_editable_layer_only.canRunUsingLayer(None))
self.assertFalse(action_editable_layer_only.canRunUsingLayer(self.vector_layer))
self.assertFalse(action_editable_layer_only.canRunUsingLayer(self.vector_layer2))
self.assertFalse(action_editable_layer_only.canRunUsingLayer(self.raster_layer))
if __name__ == '__main__':
unittest.main()