Skip to content

Commit

Permalink
[processing] In place editing triggers editing and select all
Browse files Browse the repository at this point in the history
If the active layer is not editable, the executor will
try to switch editing on.

If there are no selected features, the executor will
select all features before running.
  • Loading branch information
elpaso committed Oct 5, 2018
1 parent 223a87f commit ca1c65d
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 29 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
49 changes: 37 additions & 12 deletions python/plugins/processing/gui/AlgorithmExecutor.py
Expand Up @@ -73,19 +73,19 @@ def execute(alg, parameters, context=None, feedback=None):
def execute_in_place_run(alg, active_layer, 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 +95,41 @@ 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."))
# 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 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."))
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 alg.supportInPlaceEdit(active_layer):
raise QgsProcessingException(tr("Selected algorithm and parameter configuration are not compatible with in-place modifications."))
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 Down Expand Up @@ -190,16 +217,14 @@ 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.
:param alg: algorithm to run
:type alg: QgsProcessingAlgorithm
:param parameters: parameters of the algorithm
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
39 changes: 34 additions & 5 deletions tests/src/python/test_qgsprocessinginplace.py
Expand Up @@ -363,12 +363,7 @@ 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)
new_features = [f for f in input_layer.getFeatures()]
Expand Down Expand Up @@ -439,6 +434,40 @@ 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'] = QgsProcessingFeatureSourceDefinition(
self.vl.id(), True)
parameters['OUTPUT'] = 'memory:'

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

ok, _ = execute_in_place_run(
alg, self.vl, 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 ca1c65d

Please sign in to comment.