Navigation Menu

Skip to content

Commit

Permalink
Merge pull request #8111 from elpaso/in-place-select-all
Browse files Browse the repository at this point in the history
[processing][need-docs] In place editing triggers editing and select all
  • Loading branch information
elpaso committed Oct 5, 2018
2 parents 223a87f + cdf5bb6 commit e86693a
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 44 deletions.
22 changes: 11 additions & 11 deletions python/plugins/processing/ProcessingPlugin.py
Expand Up @@ -233,13 +233,13 @@ def initGui(self):

self.toolbox.processingToolbar.addSeparator()

self.editSelectedAction = QAction(
self.editInPlaceAction = QAction(
QgsApplication.getThemeIcon("/mActionProcessSelected.svg"),
self.tr('Edit Selected Features'), self.iface.mainWindow())
self.editSelectedAction.setObjectName('editSelectedFeatures')
self.editSelectedAction.setCheckable(True)
self.editSelectedAction.toggled.connect(self.editSelected)
self.toolbox.processingToolbar.addAction(self.editSelectedAction)
self.tr('Edit Features In-Place'), self.iface.mainWindow())
self.editInPlaceAction.setObjectName('editInPlaceFeatures')
self.editInPlaceAction.setCheckable(True)
self.editInPlaceAction.toggled.connect(self.editSelected)
self.toolbox.processingToolbar.addAction(self.editInPlaceAction)

self.toolbox.processingToolbar.addSeparator()

Expand All @@ -266,18 +266,18 @@ def initGui(self):
self.sync_in_place_button_state()

def sync_in_place_button_state(self, layer=None):
"""Synchronise the button state with layer state and selection"""
"""Synchronise the button state with layer state"""

if layer is None:
layer = self.iface.activeLayer()

old_enabled_state = self.editSelectedAction.isEnabled()
old_enabled_state = self.editInPlaceAction.isEnabled()

new_enabled_state = layer is not None and layer.type() == QgsMapLayer.VectorLayer and layer.isEditable() and layer.selectedFeatureCount()
self.editSelectedAction.setEnabled(new_enabled_state)
new_enabled_state = layer is not None and layer.type() == QgsMapLayer.VectorLayer
self.editInPlaceAction.setEnabled(new_enabled_state)

if new_enabled_state != old_enabled_state:
self.toolbox.set_in_place_edit_mode(new_enabled_state and self.editSelectedAction.isChecked())
self.toolbox.set_in_place_edit_mode(new_enabled_state and self.editInPlaceAction.isChecked())

def openProcessingOptions(self):
self.iface.showOptionsDialog(self.iface.mainWindow(), currentPage='processingOptions')
Expand Down
11 changes: 8 additions & 3 deletions python/plugins/processing/gui/AlgorithmDialog.py
Expand Up @@ -71,6 +71,7 @@ def __init__(self, alg, in_place=False, parent=None):

self.feedback_dialog = None
self.in_place = in_place
self.active_layer = None

self.setAlgorithm(alg)
self.setMainWidget(self.getParametersPanel(alg, self))
Expand All @@ -80,9 +81,13 @@ def __init__(self, alg, in_place=False, parent=None):
self.runAsBatchButton.clicked.connect(self.runAsBatch)
self.buttonBox().addButton(self.runAsBatchButton, QDialogButtonBox.ResetRole) # reset role to ensure left alignment
else:
self.active_layer = iface.activeLayer()
self.runAsBatchButton = None
self.buttonBox().button(QDialogButtonBox.Ok).setText('Modify Selected Features')
self.buttonBox().button(QDialogButtonBox.Close).setText('Cancel')
has_selection = self.active_layer and (self.active_layer.selectedFeatureCount() > 0)
self.buttonBox().button(QDialogButtonBox.Ok).setText(QCoreApplication.translate("AlgorithmDialog", "Modify Selected Features")
if has_selection else QCoreApplication.translate("AlgorithmDialog", "Modify All Features"))
self.buttonBox().button(QDialogButtonBox.Close).setText(QCoreApplication.translate("AlgorithmDialog", "Cancel"))
self.setWindowTitle(self.windowTitle() + ' | ' + self.active_layer.name())

def getParametersPanel(self, alg, parent):
return ParametersPanel(parent, alg, self.in_place)
Expand All @@ -108,7 +113,7 @@ def getParameterValues(self):
if not param.isDestination():

if self.in_place and param.name() == 'INPUT':
parameters[param.name()] = iface.activeLayer()
parameters[param.name()] = self.active_layer
continue

try:
Expand Down
74 changes: 53 additions & 21 deletions python/plugins/processing/gui/AlgorithmExecutor.py
Expand Up @@ -41,7 +41,8 @@
QgsExpression,
QgsWkbTypes,
QgsGeometry,
QgsVectorLayerUtils)
QgsVectorLayerUtils,
QgsVectorLayer)
from processing.gui.Postprocessing import handleAlgorithmResults
from processing.tools import dataobjects
from qgis.utils import iface
Expand Down Expand Up @@ -70,22 +71,20 @@ def execute(alg, parameters, context=None, feedback=None):
return False, {}


def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=None, raise_exceptions=False):
def execute_in_place_run(alg, parameters, context=None, feedback=None, raise_exceptions=False):
"""Executes an algorithm modifying features in-place in the input layer.
The input layer must be editable or an exception is raised.
:param alg: algorithm to run
:type alg: QgsProcessingAlgorithm
:param active_layer: the editable layer
:type active_layer: QgsVectoLayer
:param parameters: parameters of the algorithm
:type parameters: dict
:param context: context, defaults to None
:param context: QgsProcessingContext, optional
:type context: QgsProcessingContext, optional
:param feedback: feedback, defaults to None
:param feedback: QgsProcessingFeedback, optional
:raises QgsProcessingException: raised when the layer is not editable or the layer cannot be found in the current project
:type feedback: QgsProcessingFeedback, optional
:param raise_exceptions: useful for testing, if True exceptions are raised, normally exceptions will be forwarded to the feedback
:type raise_exceptions: boo, default to False
:raises QgsProcessingException: raised when there is no active layer, or it cannot be made editable
:return: a tuple with true if success and results
:rtype: tuple
"""
Expand All @@ -95,14 +94,46 @@ def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=N
if context is None:
context = dataobjects.createContext(feedback)

if active_layer is None or not active_layer.isEditable():
raise QgsProcessingException(tr("Layer is not editable or layer is None."))
active_layer = parameters['INPUT']

# Run some checks and prepare the layer for in-place execution by:
# - getting the active layer and checking that it is a vector
# - making the layer editable if it was not already
# - selecting all features if none was selected
# - checking in-place support for the active layer/alg/parameters
# If one of the check fails and raise_exceptions is True an exception
# is raised, else the execution is aborted and the error reported in
# the feedback
try:
if active_layer is None:
raise QgsProcessingException(tr("There is not active layer."))

if not isinstance(active_layer, QgsVectorLayer):
raise QgsProcessingException(tr("Active layer is not a vector layer."))

if not active_layer.isEditable():
if not active_layer.startEditing():
raise QgsProcessingException(tr("Active layer is not editable (and editing could not be turned on)."))

if not alg.supportInPlaceEdit(active_layer):
raise QgsProcessingException(tr("Selected algorithm and parameter configuration are not compatible with in-place modifications."))
if not alg.supportInPlaceEdit(active_layer):
raise QgsProcessingException(tr("Selected algorithm and parameter configuration are not compatible with in-place modifications."))
except QgsProcessingException as e:
if raise_exceptions:
raise e
QgsMessageLog.logMessage(str(sys.exc_info()[0]), 'Processing', Qgis.Critical)
if feedback is not None:
feedback.reportError(getattr(e, 'msg', str(e)), fatalError=True)
return False, {}

if not active_layer.selectedFeatureIds():
active_layer.selectAll()

parameters['OUTPUT'] = 'memory:'

# Start the execution
# If anything goes wrong and raise_exceptions is True an exception
# is raised, else the execution is aborted and the error reported in
# the feedback
try:
new_feature_ids = []

Expand All @@ -123,7 +154,7 @@ def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=N
if not alg.supportInPlaceEdit(active_layer):
raise QgsProcessingException(tr("Selected algorithm and parameter configuration are not compatible with in-place modifications."))
field_idxs = range(len(active_layer.fields()))
feature_iterator = active_layer.getFeatures(QgsFeatureRequest(active_layer.selectedFeatureIds())) if parameters['INPUT'].selectedFeaturesOnly else active_layer.getFeatures()
feature_iterator = active_layer.getFeatures(QgsFeatureRequest(active_layer.selectedFeatureIds()))
step = 100 / len(active_layer.selectedFeatureIds()) if active_layer.selectedFeatureIds() else 1
for current, f in enumerate(feature_iterator):
feedback.setProgress(current * step)
Expand Down Expand Up @@ -190,15 +221,15 @@ def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=N
raise e
QgsMessageLog.logMessage(str(sys.exc_info()[0]), 'Processing', Qgis.Critical)
if feedback is not None:
feedback.reportError(getattr(e, 'msg', str(e)))
feedback.reportError(getattr(e, 'msg', str(e)), fatalError=True)

return False, {}


def execute_in_place(alg, parameters, context=None, feedback=None):
"""Executes an algorithm modifying features in-place in the active layer.
The input layer must be editable or an exception is raised.
"""Executes an algorithm modifying features in-place, if the INPUT
parameter is not defined, the current active layer will be used as
INPUT.
:param alg: algorithm to run
:type alg: QgsProcessingAlgorithm
Expand All @@ -213,10 +244,11 @@ def execute_in_place(alg, parameters, context=None, feedback=None):
:rtype: tuple
"""

parameters['INPUT'] = QgsProcessingFeatureSourceDefinition(iface.activeLayer().id(), True)
ok, results = execute_in_place_run(alg, iface.activeLayer(), parameters, context=context, feedback=feedback)
if not 'INPUT' in parameters or not parameters['INPUT']:
parameters['INPUT'] = iface.activeLayer()
ok, results = execute_in_place_run(alg, parameters, context=context, feedback=feedback)
if ok:
iface.activeLayer().triggerRepaint()
parameters['INPUT'].triggerRepaint()
return ok, results


Expand Down
2 changes: 1 addition & 1 deletion python/plugins/processing/gui/AlgorithmLocatorFilter.py
Expand Up @@ -137,7 +137,7 @@ def fetchResults(self, string, context, feedback):
# collect results in main thread, since this method is inexpensive and
# accessing the processing registry/current layer is not thread safe

if iface.activeLayer() is None or iface.activeLayer().type() != QgsMapLayer.VectorLayer or not iface.activeLayer().selectedFeatureCount() or not iface.activeLayer().isEditable():
if iface.activeLayer() is None or iface.activeLayer().type() != QgsMapLayer.VectorLayer:
return

for a in QgsApplication.processingRegistry().algorithms():
Expand Down
43 changes: 35 additions & 8 deletions tests/src/python/test_qgsprocessinginplace.py
Expand Up @@ -349,8 +349,7 @@ def _alg_tester(self, alg_name, input_layer, parameters):
alg = self.registry.createAlgorithmById(alg_name)

self.assertIsNotNone(alg)
parameters['INPUT'] = QgsProcessingFeatureSourceDefinition(
input_layer.id(), True)
parameters['INPUT'] = input_layer
parameters['OUTPUT'] = 'memory:'

old_features = [f for f in input_layer.getFeatures()]
Expand All @@ -363,14 +362,9 @@ def _alg_tester(self, alg_name, input_layer, parameters):
feedback = ConsoleFeedBack()

input_layer.rollBack()
with self.assertRaises(QgsProcessingException) as cm:
execute_in_place_run(
alg, input_layer, parameters, context=context, feedback=feedback, raise_exceptions=True)

ok = False
input_layer.startEditing()
ok, _ = execute_in_place_run(
alg, input_layer, parameters, context=context, feedback=feedback, raise_exceptions=True)
alg, parameters, context=context, feedback=feedback, raise_exceptions=True)
new_features = [f for f in input_layer.getFeatures()]

# Check ret values
Expand Down Expand Up @@ -439,6 +433,39 @@ def test_execute_in_place_run(self):
}
)

def test_select_all_features(self):
"""Check that if there is no selection, the alg will run on all features"""

self.vl.rollBack()
self.vl.removeSelection()
old_count = self.vl.featureCount()

context = QgsProcessingContext()
context.setProject(QgsProject.instance())
feedback = ConsoleFeedBack()

alg = self.registry.createAlgorithmById('native:translategeometry')

self.assertIsNotNone(alg)

parameters = {
'DELTA_X': 1.1,
'DELTA_Y': 1.1,
}
parameters['INPUT'] = self.vl
parameters['OUTPUT'] = 'memory:'

old_features = [f for f in self.vl.getFeatures()]

ok, _ = execute_in_place_run(
alg, parameters, context=context, feedback=feedback, raise_exceptions=True)
new_features = [f for f in self.vl.getFeatures()]

self.assertEqual(len(new_features), old_count)

# Check all are selected
self.assertEqual(len(self.vl.selectedFeatureIds()), old_count)

def test_multi_to_single(self):
"""Check that the geometry type is still multi after the alg is run"""

Expand Down

0 comments on commit e86693a

Please sign in to comment.