Skip to content

Commit e86693a

Browse files
authored
Merge pull request #8111 from elpaso/in-place-select-all
[processing][need-docs] In place editing triggers editing and select all
2 parents 223a87f + cdf5bb6 commit e86693a

File tree

5 files changed

+108
-44
lines changed

5 files changed

+108
-44
lines changed

python/plugins/processing/ProcessingPlugin.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,13 @@ def initGui(self):
233233

234234
self.toolbox.processingToolbar.addSeparator()
235235

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

244244
self.toolbox.processingToolbar.addSeparator()
245245

@@ -266,18 +266,18 @@ def initGui(self):
266266
self.sync_in_place_button_state()
267267

268268
def sync_in_place_button_state(self, layer=None):
269-
"""Synchronise the button state with layer state and selection"""
269+
"""Synchronise the button state with layer state"""
270270

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

274-
old_enabled_state = self.editSelectedAction.isEnabled()
274+
old_enabled_state = self.editInPlaceAction.isEnabled()
275275

276-
new_enabled_state = layer is not None and layer.type() == QgsMapLayer.VectorLayer and layer.isEditable() and layer.selectedFeatureCount()
277-
self.editSelectedAction.setEnabled(new_enabled_state)
276+
new_enabled_state = layer is not None and layer.type() == QgsMapLayer.VectorLayer
277+
self.editInPlaceAction.setEnabled(new_enabled_state)
278278

279279
if new_enabled_state != old_enabled_state:
280-
self.toolbox.set_in_place_edit_mode(new_enabled_state and self.editSelectedAction.isChecked())
280+
self.toolbox.set_in_place_edit_mode(new_enabled_state and self.editInPlaceAction.isChecked())
281281

282282
def openProcessingOptions(self):
283283
self.iface.showOptionsDialog(self.iface.mainWindow(), currentPage='processingOptions')

python/plugins/processing/gui/AlgorithmDialog.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def __init__(self, alg, in_place=False, parent=None):
7171

7272
self.feedback_dialog = None
7373
self.in_place = in_place
74+
self.active_layer = None
7475

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

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

110115
if self.in_place and param.name() == 'INPUT':
111-
parameters[param.name()] = iface.activeLayer()
116+
parameters[param.name()] = self.active_layer
112117
continue
113118

114119
try:

python/plugins/processing/gui/AlgorithmExecutor.py

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
QgsExpression,
4242
QgsWkbTypes,
4343
QgsGeometry,
44-
QgsVectorLayerUtils)
44+
QgsVectorLayerUtils,
45+
QgsVectorLayer)
4546
from processing.gui.Postprocessing import handleAlgorithmResults
4647
from processing.tools import dataobjects
4748
from qgis.utils import iface
@@ -70,22 +71,20 @@ def execute(alg, parameters, context=None, feedback=None):
7071
return False, {}
7172

7273

73-
def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=None, raise_exceptions=False):
74+
def execute_in_place_run(alg, parameters, context=None, feedback=None, raise_exceptions=False):
7475
"""Executes an algorithm modifying features in-place in the input layer.
7576
76-
The input layer must be editable or an exception is raised.
77-
7877
:param alg: algorithm to run
7978
:type alg: QgsProcessingAlgorithm
80-
:param active_layer: the editable layer
81-
:type active_layer: QgsVectoLayer
8279
:param parameters: parameters of the algorithm
8380
:type parameters: dict
8481
:param context: context, defaults to None
85-
:param context: QgsProcessingContext, optional
82+
:type context: QgsProcessingContext, optional
8683
:param feedback: feedback, defaults to None
87-
:param feedback: QgsProcessingFeedback, optional
88-
:raises QgsProcessingException: raised when the layer is not editable or the layer cannot be found in the current project
84+
:type feedback: QgsProcessingFeedback, optional
85+
:param raise_exceptions: useful for testing, if True exceptions are raised, normally exceptions will be forwarded to the feedback
86+
:type raise_exceptions: boo, default to False
87+
:raises QgsProcessingException: raised when there is no active layer, or it cannot be made editable
8988
:return: a tuple with true if success and results
9089
:rtype: tuple
9190
"""
@@ -95,14 +94,46 @@ def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=N
9594
if context is None:
9695
context = dataobjects.createContext(feedback)
9796

98-
if active_layer is None or not active_layer.isEditable():
99-
raise QgsProcessingException(tr("Layer is not editable or layer is None."))
97+
active_layer = parameters['INPUT']
98+
99+
# Run some checks and prepare the layer for in-place execution by:
100+
# - getting the active layer and checking that it is a vector
101+
# - making the layer editable if it was not already
102+
# - selecting all features if none was selected
103+
# - checking in-place support for the active layer/alg/parameters
104+
# If one of the check fails and raise_exceptions is True an exception
105+
# is raised, else the execution is aborted and the error reported in
106+
# the feedback
107+
try:
108+
if active_layer is None:
109+
raise QgsProcessingException(tr("There is not active layer."))
110+
111+
if not isinstance(active_layer, QgsVectorLayer):
112+
raise QgsProcessingException(tr("Active layer is not a vector layer."))
113+
114+
if not active_layer.isEditable():
115+
if not active_layer.startEditing():
116+
raise QgsProcessingException(tr("Active layer is not editable (and editing could not be turned on)."))
100117

101-
if not alg.supportInPlaceEdit(active_layer):
102-
raise QgsProcessingException(tr("Selected algorithm and parameter configuration are not compatible with in-place modifications."))
118+
if not alg.supportInPlaceEdit(active_layer):
119+
raise QgsProcessingException(tr("Selected algorithm and parameter configuration are not compatible with in-place modifications."))
120+
except QgsProcessingException as e:
121+
if raise_exceptions:
122+
raise e
123+
QgsMessageLog.logMessage(str(sys.exc_info()[0]), 'Processing', Qgis.Critical)
124+
if feedback is not None:
125+
feedback.reportError(getattr(e, 'msg', str(e)), fatalError=True)
126+
return False, {}
127+
128+
if not active_layer.selectedFeatureIds():
129+
active_layer.selectAll()
103130

104131
parameters['OUTPUT'] = 'memory:'
105132

133+
# Start the execution
134+
# If anything goes wrong and raise_exceptions is True an exception
135+
# is raised, else the execution is aborted and the error reported in
136+
# the feedback
106137
try:
107138
new_feature_ids = []
108139

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

195226
return False, {}
196227

197228

198229
def execute_in_place(alg, parameters, context=None, feedback=None):
199-
"""Executes an algorithm modifying features in-place in the active layer.
200-
201-
The input layer must be editable or an exception is raised.
230+
"""Executes an algorithm modifying features in-place, if the INPUT
231+
parameter is not defined, the current active layer will be used as
232+
INPUT.
202233
203234
:param alg: algorithm to run
204235
:type alg: QgsProcessingAlgorithm
@@ -213,10 +244,11 @@ def execute_in_place(alg, parameters, context=None, feedback=None):
213244
:rtype: tuple
214245
"""
215246

216-
parameters['INPUT'] = QgsProcessingFeatureSourceDefinition(iface.activeLayer().id(), True)
217-
ok, results = execute_in_place_run(alg, iface.activeLayer(), parameters, context=context, feedback=feedback)
247+
if not 'INPUT' in parameters or not parameters['INPUT']:
248+
parameters['INPUT'] = iface.activeLayer()
249+
ok, results = execute_in_place_run(alg, parameters, context=context, feedback=feedback)
218250
if ok:
219-
iface.activeLayer().triggerRepaint()
251+
parameters['INPUT'].triggerRepaint()
220252
return ok, results
221253

222254

python/plugins/processing/gui/AlgorithmLocatorFilter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def fetchResults(self, string, context, feedback):
137137
# collect results in main thread, since this method is inexpensive and
138138
# accessing the processing registry/current layer is not thread safe
139139

140-
if iface.activeLayer() is None or iface.activeLayer().type() != QgsMapLayer.VectorLayer or not iface.activeLayer().selectedFeatureCount() or not iface.activeLayer().isEditable():
140+
if iface.activeLayer() is None or iface.activeLayer().type() != QgsMapLayer.VectorLayer:
141141
return
142142

143143
for a in QgsApplication.processingRegistry().algorithms():

tests/src/python/test_qgsprocessinginplace.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,7 @@ def _alg_tester(self, alg_name, input_layer, parameters):
349349
alg = self.registry.createAlgorithmById(alg_name)
350350

351351
self.assertIsNotNone(alg)
352-
parameters['INPUT'] = QgsProcessingFeatureSourceDefinition(
353-
input_layer.id(), True)
352+
parameters['INPUT'] = input_layer
354353
parameters['OUTPUT'] = 'memory:'
355354

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

365364
input_layer.rollBack()
366-
with self.assertRaises(QgsProcessingException) as cm:
367-
execute_in_place_run(
368-
alg, input_layer, parameters, context=context, feedback=feedback, raise_exceptions=True)
369-
370365
ok = False
371-
input_layer.startEditing()
372366
ok, _ = execute_in_place_run(
373-
alg, input_layer, parameters, context=context, feedback=feedback, raise_exceptions=True)
367+
alg, parameters, context=context, feedback=feedback, raise_exceptions=True)
374368
new_features = [f for f in input_layer.getFeatures()]
375369

376370
# Check ret values
@@ -439,6 +433,39 @@ def test_execute_in_place_run(self):
439433
}
440434
)
441435

436+
def test_select_all_features(self):
437+
"""Check that if there is no selection, the alg will run on all features"""
438+
439+
self.vl.rollBack()
440+
self.vl.removeSelection()
441+
old_count = self.vl.featureCount()
442+
443+
context = QgsProcessingContext()
444+
context.setProject(QgsProject.instance())
445+
feedback = ConsoleFeedBack()
446+
447+
alg = self.registry.createAlgorithmById('native:translategeometry')
448+
449+
self.assertIsNotNone(alg)
450+
451+
parameters = {
452+
'DELTA_X': 1.1,
453+
'DELTA_Y': 1.1,
454+
}
455+
parameters['INPUT'] = self.vl
456+
parameters['OUTPUT'] = 'memory:'
457+
458+
old_features = [f for f in self.vl.getFeatures()]
459+
460+
ok, _ = execute_in_place_run(
461+
alg, parameters, context=context, feedback=feedback, raise_exceptions=True)
462+
new_features = [f for f in self.vl.getFeatures()]
463+
464+
self.assertEqual(len(new_features), old_count)
465+
466+
# Check all are selected
467+
self.assertEqual(len(self.vl.selectedFeatureIds()), old_count)
468+
442469
def test_multi_to_single(self):
443470
"""Check that the geometry type is still multi after the alg is run"""
444471

0 commit comments

Comments
 (0)